何时使用Partitioner类?

46

有人可以提供使用.NET 4.0中引入的Partitioner类的典型场景吗?

3个回答

46

Partitioner类用于使并行执行更加高效。如果您需要并行运行很多非常小的任务,那么为每个任务调用委托的开销可能是禁止的。通过使用 Partitioner ,您可以将工作负载重新排列成块,并使每个并行调用在稍大的数据集上工作。该类抽象了此功能,并能够基于数据集的实际条件和可用内核进行分区。

例子:假设您想要并行运行类似以下简单计算的操作:

Parallel.ForEach(Input, (value, loopState, index) => { Result[index] = value*Math.PI; });

这将为Input中的每个条目调用委托。这样做会增加一些开销。通过使用Partitioner我们可以像这样处理

Parallel.ForEach(Partitioner.Create(0, Input.Length), range => {
   for (var index = range.Item1; index < range.Item2; index++) {
      Result[index] = Input[index]*Math.PI;
   }
});

将每次调用的操作对象增大,可以减少调用的次数。在我个人的经验中,当并行化非常简单的操作时,这可以显著提高性能。


11
Partitioner.Create()的默认rangeSize为1。因此,这两个代码示例的分区相同。除非使用Partitioner.Create(0, Input.Length, i)且i>1,否则仍将具有相同数量的线程。 - Pingpong
2
@Pingpong,目前看来这似乎不正确。我发现默认的分区器在我的8处理器机器上创建了24个块。这比输入的长度要小得多。我_想_了解它如何确定这个默认值。 - Terrence
Brian,你能给出“很多”的大致估计吗?1000个任务是问题吗?还是1000个可以,但100万个值得使用分区器? - Thomas Weller

8

范围分区是一种建议在工作量需要大量CPU计算,相对于虚拟方法调用来说较小,需要处理许多元素且每个元素的运行时间大致相同的情况下使用的一种分区类型,由Brian Rasmussen提出。

应该考虑另一种分区类型——块分区。这种类型的分区也被称为负载平衡算法,因为当还有更多的工作要做时,工作线程很少会空闲——而这不适用于范围分区。

当工作存在某些等待状态、每个元素需要更多的处理或每个元素的工作处理时间明显不同时,应该使用块分区。

这种情况的一个例子可能是读取并处理大小差异巨大的100个文件。1K文件的处理时间要比1MB文件短得多。如果使用范围分区,则一些线程可能会因为它们恰好处理了较小的文件而空闲一段时间。

与范围分区不同,除非您编写自己的自定义分区器,否则无法指定每个任务要处理的元素数量。使用块分区的另一个缺点是,在返回获取另一个块时可能会存在一些争用,因为此时使用了独占锁。因此,显然,块分区不应用于短时间的CPU密集型工作。
默认的块分区器从每个块的1个元素开始。在每个线程处理三个1元素块后,块大小增加到每个块的2个元素。在每个线程处理了三个2元素块之后,块大小再次增加到每个块的3个元素,依此类推。至少这是根据微软员工Dixin Yan(请参见块分区部分)的说法。

顺便提一下,他博客中的漂亮的可视化工具似乎是并发可视化分析器该工具的文档声称它可以用于定位性能瓶颈、CPU未充分利用、线程争用、跨核心线程迁移、同步延迟、DirectX活动、重叠I/O区域以及其他信息。它提供图形、表格和文本数据视图,显示应用程序中线程与整个系统之间的关系。

其他资源:

MSDN:PLINQ和TPL的自定义分区器

第5部分:并行编程-优化PLINQ by Joseph Albahari


2
为了在数据源上并行执行操作,其中一个关键步骤是将源分成多个部分,以便多个线程可以同时访问。当您编写并行查询或ForEach循环时,PLINQ和任务并行库(TPL)提供默认分区程序,可以透明地工作。对于更高级的场景,您可以插入自己的分区程序。
阅读更多信息请点击这里

2
我要补充的是:一般来说, 不使用它。 PLINQ 使用它,并且您可能可以使用默认分区程序。 - Roger Lipscombe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接