Make Reactive Extensions Buffer等待异步操作完成

时间:2013-06-12 22:00:10

标签: c# .net task-parallel-library system.reactive

我正在使用Reactive Extensions(Rx)来缓冲一些数据。我遇到了一个问题,因为我需要做一些与这些数据异步的事情,但我不希望缓冲区通过下一组直到异步操作完成。

我试图用两种方式构建代码(人为的例子):

public async Task processFiles<File>(IEnumerable<File> files)
{
    await files.ToObservable()
        .Buffer(10)
        .SelectMany(fi => fi.Select(f => upload(f)) //Now have an IObservable<Task>
        .Select(t => t.ToObservable())
        .Merge()
        .LastAsync();
}

public Task upload(File item)
{
    return Task.Run(() => { //Stuff });
}

public async Task processFiles<File>(IEnumerable<File> files)
{
    var buffered = files.ToObservable()
        .Buffer(10);

    buffered.Subscribe(async files => await Task.WhenAll(files.Select(f => upload(f)));

    await buffered.LastAsync();
}

public Task upload(File item)
{
    return Task.Run(() => { //Stuff });
}

不幸的是,这些方法都没有起作用,因为缓冲区在异步操作完成之前推送下一组。目的是让每个缓冲组异步执行,并且仅在该操作完成时继续执行下一个缓冲组。

非常感谢任何帮助。

2 个答案:

答案 0 :(得分:1)

为了确保我理解正确,听起来你想要确保你继续缓冲项目,同时只在前一个缓冲区被处理时呈现每个缓冲区。

您还需要使每个缓冲区的处理异步。

考虑一些理论观点可能很有价值,因为我必须承认我对这种方法有点困惑。 IObservable通常被认为是IEnumerable的双重因素,因为它反映后者的关键区别在于数据被推送到消费者而不是消费者它选择它

您正在尝试使用缓冲流,如IEnumerable而不是IObservable - 您基本上想要拉缓冲区而不是将它们推送给您 - 所以我不得不怀疑您是否选择了正确的工具来完成工作?在处理缓冲区时,您是否试图阻止缓冲操作本身?作为向您推送数据的消费者,这不是一个真正正确的方法。

您可以考虑对缓冲区操作应用ToEnumerable()调用,以便在准备好时可以处理缓冲区。当你处理当前的缓冲区时,这不会阻止正在进行的缓冲。

你可以采取一些措施来防止这种情况 - 在应用于缓冲区的Select()操作中进行缓冲处理同步将保证不会发生后续{{1调用将在OnNext()投影完成之前发生。由于Rx库操作符强制执行Rx语法,因此保证免费提供。但它只保证Select()的非重叠调用 - 没有什么可说的,给定的运营商不能(实际上不应该)继续获得下一个OnNext() {1}}准备好了。这就是基于推送的API的本质。

如果你还想阻止缓冲区,你还不清楚为什么你认为你需要将异步投影变为异步?想一想 - 我怀疑在你的观察者中使用同步OnNext()可能会解决问题,但你的问题并不完全清楚。

与同步Select()类似,是一个同步Select()处理程序,如果您的项目处理没有结果,则更容易处理 - 但它不一样,因为(取决于实现) Observable)您只阻止向该订阅者而不是所有订阅者发送OnNext()个调用。但是,只有一个订阅者它是等价的,所以你可以这样做:

OnNext()

哪些输出(*不保证在缓冲区内的过程文件x 的顺序):

void Main()
{
    var source = Observable.Range(1, 4);

    source.Buffer(2)
        .Subscribe(i =>
    {
        Console.WriteLine("Start Processing Buffer");
        Task.WhenAll(from n in i select DoUpload(n)).Wait();
        Console.WriteLine("Finished Processing Buffer");
    });
}

private Task DoUpload(int i)
{
    return Task.Factory.StartNew(
        () => {
            Thread.Sleep(1000);
            Console.WriteLine("Process File " + i);
        });
}

如果您希望使用Start Processing Buffer Process File 2 Process File 1 Finished Processing Buffer Start Processing Buffer Process File 3 Process File 4 Finished Processing Buffer 并且您的预测没有返回任何结果,则可以这样做:

Select()

NB:用LINQPad编写的示例代码,包括Nuget包Rx-Main。此代码仅用于说明目的 - 不要在生产代码中source.Buffer(2) .Select(i => { Console.WriteLine("Start Processing Buffer"); Task.WhenAll(from n in i select DoUpload(n)).Wait(); Console.WriteLine("Finished Processing Buffer"); return Unit.Default; }).Subscribe();

答案 1 :(得分:1)

首先,我认为您需要并行执行每个组中的项目,但是每个组都是非常不寻常的。更常见的要求是并行执行项目,但最多同时执行n项。这样,没有固定的组,因此如果单个项目花费的时间太长,则其他项目不必等待它。

为了做你想要的,我认为TPL Dataflow比Rx更合适(尽管一些Rx代码仍然有用)。 TPL Dataflow以执行内容的“块”为中心,默认为串行,这正是您所需要的。

您的代码可能如下所示:

public static class Extensions
{
    public static Task ExecuteInGroupsAsync<T>(
         this IEnumerable<T> source, Func<T, Task> func, int groupSize)
     {
         var block = new ActionBlock<IEnumerable<T>>(
             g => Task.WhenAll(g.Select(func)));
         source.ToObservable()
               .Buffer(groupSize)
               .Subscribe(block.AsObserver());
         return block.Completion;
     }
}

public Task ProcessFiles(IEnumerable<File> files)
{
    return files.ExecuteInGroupsAsync(Upload, 10);
}

这使得大部分繁重的工作都留在了ActionBlock(以及一些在Rx上)。数据流块可以充当Rx观察者(和可观察者),因此我们可以利用它来继续使用Buffer()

我们希望一次处理整个组,因此我们使用Task.WhenAll()创建一个Task,在整个组完成时完成。数据流块理解Task - 返回函数,因此在前一组返回的Task完成之前,下一组将不会开始执行。

最终结果是Completion Task,它将在源观察完成并完成所有处理后完成。

TPL数据流也有BatchBlock,其作用类似于Buffer(),我们可以直接Post()来自该集合的每个项目(不使用ToObservable()AsObserver()) ,但我认为在代码的这一部分使用Rx会使其更简单。

编辑:实际上,您根本不需要TPL数据流。使用詹姆斯世界建议的ToEnumerable()就足够了:

public static async Task ExecuteInGroupsAsync<T>(
     this IEnumerable<T> source, Func<T, Task> func, int groupSize)
{
    var groups = source.ToObservable().Buffer(groupSize).ToEnumerable();
    foreach (var g in groups)
    {
        await Task.WhenAll(g.Select(func));
    }
}

甚至在没有使用来自morelinqBatch()的Rx时更简单:

public static async Task ExecuteInGroupsAsync<T>(
    this IEnumerable<T> source, Func<T, Task> func, int groupSize)
{
    var groups = source.Batch(groupSize);
    foreach (var group in groups)
    {
        await Task.WhenAll(group.Select(func));
    }
}