如何提高Parallel.ForEach的吞吐量

时间:2013-11-19 16:01:29

标签: c# .net-4.0 parallel.foreach

我尝试使用并行执行来优化代码,但有时只有一个线程会承受所有重负载。以下示例显示了如何在最多4个线程中执行40个任务,并且前10个任务比其他线程更耗时。

Parallel.ForEach似乎将数组拆分为4个部分,并让一个线程处理每个部分。所以整个执行大约需要10秒钟。它应该能够在最多3.3秒内完成!

有没有办法一直使用所有线程,因为在我的真正问题中我不知道哪些任务是耗时的?

var array = System.Linq.Enumerable.Range(0, 40).ToArray();

System.Threading.Tasks.Parallel.ForEach(array, new System.Threading.Tasks.ParallelOptions() { MaxDegreeOfParallelism = 4, },
     i =>
     {
         Console.WriteLine("Running index {0,3} : {1}", i, DateTime.Now.ToString("HH:mm:ss.fff"));
         System.Threading.Thread.Sleep(i < 10 ? 1000 : 10);
     });

4 个答案:

答案 0 :(得分:4)

使用Parallel.ForEach可能可能,但您需要使用自定义分区程序(或查找第三方分区程序),以便能够根据您的内容更明智地对元素进行分区特别的项目。 (或者只使用更小的批次。)

这也是假设你没有事先严格知道哪些项目会很快而哪些项目很慢;如果您这样做,您可以在致电ForEach之前自行重新订购商品,以便更昂贵的商品更加分散。根据具体情况,这可能也可能不够。

一般来说,我更喜欢通过简单地拥有一个生产者和多个消费者来解决这些问题,每个消费者一次处理一个项目,而不是批处理。 BlockingCollection类使这些情况变得相当简单。只需将所有项目添加到集合中,创建N个任务/线程/等,每个项目都会抓取一个项目并对其进行处理,直到没有更多项目为止。它没有为您提供Parallel.ForEach为您提供的动态添加/删除线程,但在您的情况下这似乎不是问题。

答案 1 :(得分:4)

使用自定义分区程序是修改Parallel.ForEach()行为的正确解决方案。如果您使用的是.Net 4.5,则可以使用an overload of Partitioner.Create()。有了它,您的代码将如下所示:

var partitioner = Partitioner.Create(
    array, EnumerablePartitionerOptions.NoBuffering);
Parallel.ForEach(
    partitioner, new ParallelOptions { MaxDegreeOfParallelism = 4, }, i => …);

这不是默认设置,因为关闭缓冲会增加Parallel.ForEach()的开销。但是如果你的迭代真的那么长(秒),那么额外的开销就不应该引人注意了。

答案 2 :(得分:3)

这是由于名为partitioner的功能。默认情况下,您的循环在可用线程之间平均分配。听起来你想要改变这种行为。当前行为背后的原因是它需要一定的开销时间来设置一个线程,所以你想要做的工作和合理的工作一样多。因此,集合被分区为块并发送到每个线程。系统无法知道集合的某些部分需要比其他部分更长的时间(除非您明确告诉它)并假设相等的部分导致大致相等的完整时间。在您的情况下,您可能希望以不同的方式拆分需要更长时间和运行时间的任务。或者您可能希望提供一个自定义分区程序,以非顺序方式遍历集合。

答案 3 :(得分:2)

您可能希望使用Microsoft TPL Dataflow库,这有助于设计突出显示的并发系统。

您的代码大致相当于使用此库的以下代码:

var options = new ExecutionDataflowBlockOptions {
    MaxDegreeOfParallelism = 4,
    SingleProducerConstrained = true
};

var actionBlock = new ActionBlock<int>(i => {
    Console.WriteLine("Running index {0,3} : {1}", i, DateTime.Now.ToString("HH:mm:ss.fff"));
    System.Threading.Thread.Sleep(i < 10 ? 1000 : 10);
}, options);

Task.WhenAll(Enumerable.Range(0, 40).Select(actionBlock.SendAsync)).Wait();
actionBlock.Complete();
actionBlock.Completion.Wait();
在这种情况下,TPL数据流将使用4个消费者,只要其中一个消费者可用,就会处理新值,从而最大化吞吐量。

一旦习惯了库,您可能希望使用库提供的各种块添加更多的异步,并删除所有那些糟糕的Wait调用。