TPL 数据流:在保持秩序的同时进行并行设计

Posted

技术标签:

【中文标题】TPL 数据流:在保持秩序的同时进行并行设计【英文标题】:TPL Dataflow: design for parallelism while keeping order 【发布时间】:2014-02-26 09:30:39 【问题描述】:

我以前从未使用过 TPL,所以我想知道是否可以使用它来完成: 我的应用程序从很多帧中创建了一个 gif 图像动画文件。我从一个代表 gif 文件帧的位图列表开始,并且需要对每个帧执行以下操作:

    在框架上绘制一些文本/位图 裁剪框架 调整框架大小 将图像缩小为 256 色

显然,此过程可以针对列表中的所有帧并行完成,但对于每个帧,步骤顺序必须相同。 之后,我需要将所有帧写入 gif 文件。因此,所有帧都需要按照它们在原始列表中的相同顺序接收。最重要的是,这个过程可以在第一帧准备好时开始,不需要等到所有帧都处理完。

情况就是这样。 TPL 数据流适合这个吗?如果是的话,谁能给我一个正确方向的提示,说明如何设计 tpl 块结构以反映上述过程?与我找到的一些样本相比,这对我来说似乎相当复杂。

【问题讨论】:

【参考方案1】:

我认为为此使用 TPL 数据流是有意义的,尤其是因为它会自动将已处理的元素保持在正确的顺序,即使打开了并行性也是如此。

您可以为流程中的每个步骤创建一个单独的块,但我认为这里没有必要,一个用于处理帧的块和一个用于编写它们的块就足够了:

public Task CreateAnimationFileAsync(IEnumerable<Bitmap> frames)

    var frameProcessor = new TransformBlock<Bitmap, Bitmap>(
        frame => ProcessFrame(frame),
        new ExecutionDataflowBlockOptions
         MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded );

    var animationWriter = new ActionBlock<Bitmap>(frame => WriteFrame(frame));

    frameProcessor.LinkTo(
        animationWriter,
        new DataflowLinkOptions  PropagateCompletion = true );

    foreach (var frame in frames)
    
        frameProcessor.Post(frame);
    

    frameProcessor.Complete();

    return animationWriter.Completion;


private Bitmap ProcessFrame(Bitmap frame)

    …


private async Task WriteFrame(Bitmap frame)

    …

【讨论】:

使用多个块的好处是您可以轻松更改它们以以不同的方式处理它。 @MattCarkci 是的,这在某些情况下可能很有用。但是在这里,我认为更改ProcessFrame() 就像更改块一样简单,并且代码更简单。 你应该返回 animationWriter.ompletion 而不是 transformblock。 MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded 有点吓人。这意味着并行性将基本上受到饥饿的ThreadPool 的限制。我更喜欢MaxDegreeOfParallelism = Environment.ProcessorCount。或者也许MaxDegreeOfParallelism = Environment.ProcessorCount * 2 来平衡大块的工作负载,代价是线程切换引起的一些开销。在后一种情况下,我还将增加线程池立即产生的最小线程数:ThreadPool.SetMinThreads(Environment.ProcessorCount * 2, 10)【参考方案2】:

您的问题是数据流擅长的完美示例。

这是可以帮助您入门的最简单的代码。

// Try increasing MaxDegreeOfParallelism
var opt = new ExecutionDataflowBlockOptions  MaxDegreeOfParallelism = 2 ;

// Create the blocks
// You must define the functions to do what you want
var paintBlock = new TransformBlock<Bitmap, Bitmap>(fnPaintText, opt);
var cropBlock = new TransformBlock<Bitmap, Bitmap>(fnCrop, opt);
var resizeBlock = new TransformBlock<Bitmap, Bitmap>(fnResize, opt);
var reduceBlock = new TransformBlock<Bitmap, Bitmap>(fnReduce,opt);

// Link the blocks together
paintBlock.LinkTo(cropBlock);
cropBlock.LinkTo(resizeBlock);
resizeBlock.LinkTo(reduceBlock);

// Send data to the first block
// ListOfImages contains your original frames
foreach (var img in ListOfImages)  
   paintBlock.Post(img);


// Receive the modified images
var outputImages = new List<Bitmap>();
for (int i = 0; i < ListOfImages.Count; i++) 
   outputImages.Add(reduceBlock.Receive());


// outputImages now holds all of the frames
// reassemble them in order

【讨论】:

这是错误的。即使设置了MaxDegreeOfParallelism,Dataflow 也会自动将消息保持在正确的顺序,不需要Wrapper【参考方案3】:

我想你会发现DataFlow 是正确的方法。对于每一帧,从您的帧列表中,尝试创建一个TransformBlock。对于四个步骤中的每一个,以正确的顺序将框架链接在一起。如果您想同时处理 framelist,您可以使用 bufferblock 作为 framelist。

please find the full sample on how to use transformblock on msdn:

【讨论】:

我认为每个 frame 使用一个块没有多大意义。过程中的每个步骤可能会出现一个块。

以上是关于TPL 数据流:在保持秩序的同时进行并行设计的主要内容,如果未能解决你的问题,请参考以下文章

添加新数据时如何在保持秩序的同时重新加载collectionview

TPL 数据流是不是适用于这种设计类型?

使用任务并行库 (TPL) 进行轮询

并发编程基础

理解并行编程

维护插入顺序的 Java 集合