长时间运行流程的并行化和性能优化

时间:2012-11-14 16:55:57

标签: c# .net task-parallel-library async-await tpl-dataflow

我想并行处理逐帧处理多个视频剪辑的应用程序。每个剪辑的每个帧的序列很重要(显然)。 我决定使用TPL Dataflow,因为我相信这是数据流的一个很好的例子(电影帧是数据)。

所以我有一个从数据库加载帧的进程(比方说,一批500个,全部聚集在一起)

Example sequence:    
|mid:1 fr:1|mid:1 fr:2|mid:2 fr:1|mid:3 fr:1|mid:1 fr:3|mid:2 fr:2|mid:2 fr:3|mid:1 fr:4|

并将它们发布到BufferBlock。对于这个BufferBlock,我已经将ActionBlocks与过滤器相关联,每个MovieID都有一个ActionBlock,以便我获得某种数据分区。每个ActionBlock都是顺序的,但理想情况下,多个电影的多个ActionBlock可以并行运行。

我确实让上述网络正常运行并且确实并行运行,但从我的计算中,只有8到10个ActionBlock同时执行。我计算了每个ActionBlock的运行时间和大约100-200ms。 我可以采取哪些步骤来至少实现双重并发?

我确实尝试将操作委托转换为异步方法,并在ActionBlock操作委托中使数据库访问异步,但它没有帮助。

编辑:我实施了额外级别的数据分区:在ServerA上处理具有奇数ID的电影的帧,在ServerB上处理偶数电影的帧。应用程序的两个实例都在同一个数据库中。如果我的问题是DB IO,那么我不会看到总帧处理数量有任何改善(或者很少,低于20%)。但我确实看到它翻了一番。因此,这让我得出结论,Threadpool并没有产生更多线程来并行执行更多帧(两个服务器都是四核,而探查器显示每个应用程序大约25-30个线程)。

3 个答案:

答案 0 :(得分:2)

一些假设:

  • 根据您的示例数据,您无法接收电影帧(可能还有电影中的帧)

  • 您的ActionBlock<T>个实例是通用的;他们都调用相同的处理方法,你只需根据每个电影ID创建一个列表(你事先有一个电影ID列表),如下所示:

// The movie IDs
IEnumerable<int> movieIds = ...;

// The actions.
var actions = movieIds.Select(
    i => new { Id = i, Action = new ActionBlock<Frame>(MethodToProcessFrame) });

// The buffer block.
BufferBlock<Frame> buffer = ...;

// Link everything up.
foreach (var action in actions) 
{
    // Not necessary in C# 5.0, but still, good practice.
    // The copy of the action.
    var actionCopy = action;

    // Link.
    bufferBlock.LinkTo(actionCopy.Action, f => f.MovieId == actionCopy.Id);
}

如果是这种情况,您就会创建太多ActionBlock<T>个实例,而这些实例没有被授予工作;因为您的框架(可能还有电影)是无序的,所以您无法保证所有ActionBlock<T>实例都有工作要做。

此外,当您创建ActionBlock<T>实例时,它将以MaxDegreeOfParallelism为1创建,这意味着它的线程安全,因为只有一个线程可以访问同时阻止。

此外,TPL DataFlow库最终依赖于Task<TResult> class,它默认在线程池上进行调度。线程池将在这里做一些事情:

  • 确保所有处理器核心都已饱和。这非常与确保您的ActionBlock<T>个实例已经饱和且是您应关注的指标

  • 不同
  • 确保在处理器内核饱和时,确保工作均匀分布,并确保不执行太多许多并发任务(上下文切换很昂贵) )。

看起来你处理电影的方法也是通用的,传入电影的帧是什么并不重要(如果 重要,那么你需要更新你的问题,因为它改变了很多东西)。这也意味着它是线程安全的。

此外,如果可以假设一帧的处理不依赖于任何先前帧的处理(或者,看起来电影的帧看起来有序),则可以使用 ActionBlock<T>但调整MaxDegreeOfParallelism值,如下所示:

// The buffer block.
BufferBlock<Frame> buffer = ...;

// Have *one* ActionBlock<T>
var action = new ActionBlock<Frame>(MethodToProcessFrame,
    // This is where you tweak the concurrency:
    new ExecutionDataflowBlockOptions {
        MaxDegreeOfParallelism = 4,
    }
);

// Link.  No filter needed.
bufferBlock.LinkTo(action);

现在,您的ActionBlock<T>始终饱和。当然,任何负责任的任务调度程序(默认情况下是线程池)仍然会限制最大并发数量,但它会尽可能多地同时执行。

为此,如果您的操作真正线程安全,则可以将MaxDegreeOfParallelism设置为DataflowBlockOptions.Unbounded,如下所示:

// Have *one* ActionBlock<T>
var action = new ActionBlock<Frame>(MethodToProcessFrame,
    // This is where you tweak the concurrency:
    new ExecutionDataflowBlockOptions {
        // We're thread-safe, let the scheduler determine
        // how nuts we can go.
        MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded,
    }
);

当然,所有这些都假定其他一切都是最优的(I / O读/写等)

答案 1 :(得分:-1)

可能性是最佳的并行化程度。实际上,线程池非常适合确定有效的实际线程的最佳数量。我的猜测是你的硬件可以支持许多实际并行工作的并行进程。如果你添加了更多,你实际上不会增加​​吞吐量,你只需花费更多的时间在线程之间进行上下文切换,而花费更少的时间来实际处理它们。

如果您注意到,在很长一段时间内,您的CPU负载,内存总线,网络连接,磁盘访问等都在低于容量的情况下运行,那么您可能会遇到问题,并且您需要检查看看实际上是什么瓶颈。有可能某些资源处于某种程度上,并且TPL已经认识到这一点,并确保它不会过度饱和该资源。

答案 2 :(得分:-1)

我怀疑你是IO界限。问题是在哪里?在读或写。你在写更多的数据而不是阅读。 CPU可能低于50%,因为它无法写得更快。

我不是说ActionBlock是错的,但我会考虑使用BlockingCollection的生产者消费者。优化读写数据的方式。

这个不同但我有一个应用程序,我在其中阅读文本块。解析文本,然后将单词写回SQL。我在单个线程上读取,然后并行解析,然后在单个线程上写入。我在一个线程上写,以免破坏索引。如果您是IO绑定,您需要弄清楚什么是最慢的IO然后优化该过程。

告诉我有关IO的更多信息。

在你提出的问题中你也提到了数据库 我会试试BlockingCollections BlockingCollection Class
并且每个都有尺寸限制,因此您不会记​​忆 使它足够大,几乎不会变空 最慢步骤后的阻塞集合将变为空。 如果你可以并行处理,那么这样做 我发现表中的平行插入物并不快 让一个过程锁定并保持它并保持软管打开 仔细看看你如何插入。
一次一行很慢 我使用TVP并一次插入10,000个,但很多人喜欢Drapper或BulkInsert 如果删除索引和触发器,则按聚簇索引排序的插入将是最快的。 拿一个锁扣并握住它。 我在10毫秒范围内插入插件 现在更新速度最慢。 看看那个 - 你一次只做一行吗? 看看制片和视频剪辑 除非它是一个丑陋的更新,它不应该比插入更长的时间。