我在不同的任务上运行一个非常典型的生产者/消费者模型。
任务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中的集合批次的顺序,并在工作任务完成后重新合并它们吗?它是否优化了什么?在对整个结构进行分析之后,我觉得由于旋转和涉及太多并发集合而浪费了相当多的时间。
任何想法,想法?
答案 0 :(得分:12)
编辑:原来我错了。 TransformBlock
以相同的顺序返回项目,即使它是针对并行性配置的。因此,原始答案中的代码完全无用,可以使用普通TransformBlock
。
原始答案:
据我所知,.Net中只有一个并行构造支持按照它们进入的顺序返回已处理的项目:PLINQ AsOrdered()
。但在我看来,PLINQ并不适合你想要的东西。
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库非常适合此工作。它支持您需要的所有功能:MaxDegreeOfParallelism
,BoundedCapacity
和EnsureOrdered
。但是使用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); }
}