【问题标题】:Parallelization of long running processes and performance optimization长时间运行进程的并行化和性能优化
【发布时间】:2012-11-14 16:55:56
【问题描述】:

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

所以我有一个从数据库加载帧的进程(可以说是一批 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 可以并行运行。

我确实有上述网络工作并且它确实并行运行,但根据我的计算,只有八到十个动作块同时执行。我计时了每个 ActionBlock 的运行时间,大约为 100-200 毫秒。 我可以采取哪些步骤来至少双倍并发?

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

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

【问题讨论】:

  • 您的操作块是否将任何数据写入磁盘或返回 SQL?你的动作块有任何 IO 吗?这可以通过从 SQL 获取数据来限制。当您说 CPU 低于 50% 是平均值或保持低于 50% 时。
  • ActionBlock 从/向 SQL 读取/写入数据。 CPU 确实保持在 50% 以下,平均为 25%。我确实将数据库访问方法转换为异步,它的生产力提高了大约 20%。但仍然认为我可以做得更好。
  • Parallel Stacks 确实显示了大约 18-20 个线程池线程。如果我能把这个提高到 50,我会是一个快乐的露营者。
  • “我可以采取哪些步骤来至少双倍并发?”恕我直言,你不能只说“我想加倍 X”,这意味着它可以完成。是否可能取决于您在此处列出的更多因素。充其量,我们可以为您提供有关如何改进并发性或整体处理的指导。

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


【解决方案1】:

一些假设:

  • 根据您的示例数据,您正在接收乱序的电影帧(可能还有电影中的帧)

  • 您的 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&lt;T&gt; 实例;因为您的帧(可能还有电影)是乱序的,所以您不能保证所有 ActionBlock&lt;T&gt; 实例都会有工作要做。

此外,当您创建一个ActionBlock&lt;T&gt; 实例时,它的MaxDegreeOfParallelism 将被创建为1,这意味着它是线程安全的,因为只有一个线程可以同时访问该块。

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

  • 确保所有处理器内核都已饱和。这非常不同于确保您的ActionBlock&lt;T&gt; 实例已饱和,而是您应该关注的指标

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

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

此外,如果可以假设一帧的处理不依赖于任何先前帧的处理(或者,看起来电影的帧是按顺序排列的),您可以使用 single ActionBlock&lt;T&gt; 但调整 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&lt;T&gt;总是饱和。诚然,任何负责任的任务调度程序(默认为线程池)仍会限制最大并发量,但它会同时尽可能多地做。

为此,如果您的操作是真正线程安全的,您可以将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 帧之前处理电影 1 的第 2 帧。我依靠第 1 帧处理的输出来处理同一部电影的第 2 帧。因此,处理电影的顺序无关紧要,但任何给定电影的帧顺序都很重要。这就是我使用 ActionBlocks 和 Filtered Links 对数据进行分区的原因
  • 另一方面,使用我当前的架构,我注释掉了数据访问代码,使生产力翻了一番。所以这让我相信这是一个数据库 IO 问题
  • @Dimitri 如果它 is 是一个 DB IO 问题,那么这会使这个问题“过于本地化”,因此应该关闭它。我错过了关于序列的部分(这并不明显,因为并非所有视频处理都依赖于最后一个剪辑,这取决于您要执行的操作,因为输出可以重新排序),我将重新处理帐户的答案为此,但是如果您按顺序将其输入BufferBlock&lt;T&gt;,那么您将按该顺序喂它。您只需要确保您没有同时处理同一部电影中的帧。此外,这显然会限制您获得的并发量。
  • 我可能会尝试将数据分区从每部电影更改为每个奇数电影 ID 和偶数电影 ID,并利用某种阻塞来避免对同一部电影的帧进行并行处理
  • @Dimitri 你在帧之间有多少依赖关系?一帧的处理是否依赖于前一帧的处理?您或许可以使用 TPL 为您处理此聚合。
【解决方案2】:

很可能这是最佳的并行化程度。老实说,线程池非常擅长确定要激活的实际线程的最佳数量。我的猜测是您的硬件可以支持实际上并行工作的许多并行进程。如果您添加更多内容,您实际上不会增加​​吞吐量,您只会花费更多时间在线程之间进行上下文切换,而实际处理它们的时间会减少。

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

【讨论】:

  • 是的,我的 CPU 负载在 50% 以下,网络在 15% 左右,内存在 70%。我确实觉得有一个地方可以成长。例如,如果我启动同一个应用程序的两个实例,我可能会加倍输出。但我讨厌运行应用程序的多个实例。想把它们放在一个屋檐下
  • 我还阅读了一篇关于 TPL Dataflow 反对在操作块中使用 TaskScheduler.Default(线程池)的帖子,但我不记得我在哪里看到了该线程。它表示线程池在产生新线程方面非常悲观并提供了替代方案,但不记得是什么(不是 ConcurrentScheduler)
【解决方案3】:

我怀疑你是 IO 绑定的。问题是在哪里?在读或写上。你写的数据比读的多吗? CPU 可能低于 50%,因为它无法更快地写入。

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

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

告诉我更多关于那个 IO 的信息。

在您提到的从数据库中读取的问题中。
我会试试 BlockingCollections。
BlockingCollection Class
并且每个都有大小限制,这样你就不会浪费内存。
让它足够大,以至于它(几乎)永远不会变空。
最慢步骤后的阻塞集合将变为空。 如果您可以并行处理,那么就这样做。
我发现表中的并行插入并不快。
让一个进程锁定并保持它并保持软管打开。
仔细看看你是如何插入的。
一次一排很慢。
我使用 TVP 并一次插入 10,000 个,但很多人喜欢 Drapper 或 BulkInsert。
如果您删除索引和触发器并插入按聚集索引排序将是最快的。 拿一个小块并握住它。 我得到了 10 毫秒范围内的插入。
现在更新是最慢的。 看看那个 - 你一次只做一排吗?
看看拿tabblock和做视频剪辑。
除非它是一个丑陋的更新,否则它不应该比插入花费更长的时间。

【讨论】:

  • ActionBlock 由三个带有 DB IO 的段组成: 1) 插入 TableA (40-50ms); 2)插入TableB(50-60mx); 3)更新TableC和TableD(70-80ms)。第 3 步依赖于第 2 步,因此我无法并行运行这三个步骤。
  • 我已经使用了容量受限的 DataFlow BufferBlock。批量插入不是一种选择,因为一部电影的一帧不能等到另一部电影的第二帧完成。加上插入后的更新取决于插入,所以我不能轻易地为插入触发一个单独的线程。而且我不能在表上删除索引,因为该表也被访问以进行读取(读取次数多于插入次数)。
  • 不明白批处理如何阻止您处理多部电影 - 只需将结果保存在集合中。而且不明白将依赖于插入的更新放在单独的线程中是多么困难。
  • 好吧,要进行批处理,我必须等待第 N 帧才能发出批处理插入,对吗?!我是说等待第 N 帧不是一种选择。这不是一个真正的电影处理器应用程序,但我选择了电影处理作为示例,因为它非常适合。
  • N 不需要是常数。一次处理一行是最慢的方法。如果您不能在集合中保存“帧”,那么您将陷入缓慢的 SQL IO。
猜你喜欢
  • 2023-01-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多