这是TPL Dataflow的工作吗?

时间:2012-06-15 14:46:31

标签: c# concurrency task-parallel-library producer-consumer tpl-dataflow

我在不同的任务上运行一个非常典型的生产者/消费者模型。

任务1:从二进制文件中读取批量的byte [],并为每个字节数组集合启动一个新任务。 (该操作是为了内存管理目的而批量处理的。)

任务2-n:这些是工作任务,每个都在字节数组的传入集合(来自Tasks1)上运行,并对字节数组进行反序列化,按特定条件对它们进行排序,然后存储结果对象的集合(每个字节数组在并发字典中反序列化为此类对象。

任务(n + 1)我选择了并发字典,因为此任务的工作是以与它们来自Task1的顺序相同的顺序合并存储在并发字典中的那些集合。我通过传递一个collectionID(它是int类型并为Task1中的每个新集合递增)从Task1到此任务一直向下实现。此任务基本上检查下一个预期的collectionID是否已存储在并发字典中,如果是,则将其取出,将其添加到Final Queue并检查并发字典中的下一个集合。

现在,从我所看到的内容和我观看的视频来看,TPL Dataflow可能是这种生产者/消费者模型的完美候选者。我似乎无法设计并因此开始,因为我从未使用过TPL Dataflow。在吞吐量和延迟方面,这个库甚至可以完成任务吗?我目前处理250万字节数组,因此在生成的集合中每秒处理对象。 TPL Dataflow可以帮助简化吗?我对以下问题的答案特别感兴趣:TPL Dataflow可以在产生工作任务时保留Task1中的集合批次的顺序,并在工作任务完成后重新合并它们吗?它是否优化了什么?在对整个结构进行分析之后,我觉得由于旋转和涉及太多并发集合而浪费了相当多的时间。

任何想法,想法?

2 个答案:

答案 0 :(得分:12)

编辑:原来我错了。 TransformBlock 以相同的顺序返回项目,即使它是针对并行性配置的。因此,原始答案中的代码完全无用,可以使用普通TransformBlock


原始答案:

据我所知,.Net中只有一个并行构造支持按照它们进入的顺序返回已处理的项目:PLINQ AsOrdered()。但在我看来,PLINQ并不适合你想要的东西。

另一方面,TPL Dataflow很适合,但是它没有一个块可以同时支持并行和返回项目(TransformBlock支持它们,但是不是在同一时间)。幸运的是,Dataflow块在设计时考虑了可组合性,因此我们可以构建自己的块来实现这一点。

但首先,我们必须弄清楚如何订购结果。像你建议的那样使用并发字典以及一些同步机制肯定会起作用。但我认为有一个更简单的解决方案:使用Task的队列。在输出任务中,您将Task出列,等待它完成(异步),当它出现时,您将其结果发送出去。当队列为空时,我们仍然需要一些同步,但如果我们选择要巧妙使用哪个队列,我们​​可以免费获得。

所以,一般的想法是这样的:我们写的是IPropagatorBlock,有一些输入和一些输出。创建自定义IPropagatorBlock的最简单方法是创建一个处理输入的块,另一个块生成结果,并使用DataflowBlock.Encapsulate()将它们视为一个。

输入块必须以正确的顺序处理传入的项目,因此没有并行化。它会创建一个新的Task(实际上是TaskCompletionSource,以便我们可以稍后设置Task的结果),将其添加到队列中然后发送项目进行处理,以及一些设置正确Task结果的方法。因为我们不需要将此块链接到任何内容,所以我们可以使用ActionBlock

输出块必须从队列中取Task个,异步等待它们,然后发送它们。但由于所有块都嵌入了一个队列,并且带有委托的块具有内置的异步等待,因此这将非常简单:new TransformBlock<Task<TOutput>, TOutput>(t => t)。该块既可用作队列,也可用作输出块。因此,我们不必处理任何同步。

最后一块拼图实际上是并行处理这些物品。为此,我们可以使用另一个ActionBlock,这次设置为MaxDegreeOfParallelism。它将接受输入,处理它,并在队列中设置正确的Task的结果。

放在一起,看起来像这样:

public static IPropagatorBlock<TInput, TOutput>
    CreateConcurrentOrderedTransformBlock<TInput, TOutput>(
    Func<TInput, TOutput> transform)
{
    var queue = new TransformBlock<Task<TOutput>, TOutput>(t => t);

    var processor = new ActionBlock<Tuple<TInput, Action<TOutput>>>(
        tuple => tuple.Item2(transform(tuple.Item1)),
        new ExecutionDataflowBlockOptions
        {
            MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded
        });

    var enqueuer = new ActionBlock<TInput>(
        async item =>
        {
            var tcs = new TaskCompletionSource<TOutput>();
            await processor.SendAsync(
                new Tuple<TInput, Action<TOutput>>(item, tcs.SetResult));
            await queue.SendAsync(tcs.Task);
        });

    enqueuer.Completion.ContinueWith(
        _ =>
        {
            queue.Complete();
            processor.Complete();
        });

    return DataflowBlock.Encapsulate(enqueuer, queue);
}

经过这么多的讨论,我认为这是相当少量的代码。

您似乎非常关心性能,因此您可能需要对此代码进行微调。例如,将MaxDegreeOfParallelism块的processor设置为类似Environment.ProcessorCount的内容可能有意义,以避免超额订阅。此外,如果延迟比吞吐量对您更重要,那么将同一块的MaxMessagesPerTask设置为1(或另一个小数字)可能是有意义的,这样当项目处理完成后,它就会被发送到输出立即

此外,如果您想限制传入的项目,可以设置BoundedCapacity的{​​{1}}。

答案 1 :(得分:0)

是的,TPL Dataflow库非常适合此工作。它支持您需要的所有功能:MaxDegreeOfParallelismBoundedCapacityEnsureOrdered。但是使用BoundedCapacity选项需要注意一些细节。

首先,您必须确保使用SendAsync方法将流水线中的第一个块送入。否则,如果您使用Post方法而忽略了它的返回值,则可能会丢失消息。 SendAsync永远不会丢失消息,因为它异步阻塞了调用者,直到块的内部缓冲区中的传入消息有可用空间为止。

其次,您必须确保下游块中可能出现的异常不会无限期地阻塞供料器,等待永远不会出现的可用空间。没有内置的方法可以通过配置块来自动完成此操作。相反,您必须手动将下游块的完成传播到上游块。这是下面示例中的方法PropagateFailure的目的:

public static async Task ProcessAsync(string[] filePaths,
    ConcurrentQueue<MyClass> finalQueue)
{
    var reader = new TransformBlock<string, byte[]>(filePath =>
    {
        byte[] result = ReadBinaryFile(filePath);
        return result;
    }, new ExecutionDataflowBlockOptions()
    {
        MaxDegreeOfParallelism = 1, // This is the default
        BoundedCapacity = 20, // keep memory usage under control
        EnsureOrdered = true // This is also the default
    });

    var deserializer = new TransformBlock<byte[], MyClass>(bytes =>
    {
        MyClass result = Deserialize(bytes);
        return result;
    }, new ExecutionDataflowBlockOptions()
    {
        MaxDegreeOfParallelism = Environment.ProcessorCount,
        BoundedCapacity = 20
    });

    var writer = new ActionBlock<MyClass>(obj =>
    {
        finalQueue.Enqueue(obj);
    });

    reader.LinkTo(deserializer,
        new DataflowLinkOptions() { PropagateCompletion = true });
    PropagateFailure(deserializer, reader); // Link backwards

    deserializer.LinkTo(writer,
        new DataflowLinkOptions() { PropagateCompletion = true });
    PropagateFailure(writer, deserializer); // Link backwards

    foreach (var filePath in filePaths)
    {
        var accepted = await reader.SendAsync(filePath).ConfigureAwait(false);
        if (!accepted) break; // This will happen in case that the block has failed
    }
    reader.Complete(); // This will be ignored if the block has already failed

    await writer.Completion; // This will propagate the first exception that occurred
}

public static async void PropagateFailure(IDataflowBlock block1,
    IDataflowBlock block2)
{
    try { await block1.Completion.ConfigureAwait(false); }
    catch (Exception ex) { block2.Fault(ex); }
}