C# 如何提高Parallel.ForEach上的吞吐量

C# 如何提高Parallel.ForEach上的吞吐量,c#,.net-4.0,parallel.foreach,C#,.net 4.0,Parallel.foreach,我试图通过并行执行优化代码,但有时只有一个线程会承受所有的重载。下面的示例演示了如何在最多4个线程中执行40个任务,并且前10个线程比其他线程更耗时 Parallel.ForEach似乎将数组分成4部分,并让一个线程处理每个部分。所以整个执行过程大约需要10秒。它应该能够在最多3.3秒内完成 有没有一种方法可以一直使用所有线程,因为我真正的问题是不知道哪些任务很耗时 var array = System.Linq.Enumerable.Range(0, 40).ToArray(); Syste

我试图通过并行执行优化代码,但有时只有一个线程会承受所有的重载。下面的示例演示了如何在最多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);
     });
var-array=System.Linq.Enumerable.Range(0,40).ToArray();
System.Threading.Tasks.Parallel.ForEach(数组,新的System.Threading.Tasks.ParallelOptions(){MaxDegreeOfParallelism=4,},
i=>
{
WriteLine(“运行索引{0,3}:{1}”,i,DateTime.Now.ToString(“HH:mm:ss.fff”);
系统线程线程睡眠(i<10?1000:10);
});
使用Parallel.ForEach是可能的,但您需要使用一个自定义分区器(或找到第三方分区器),该分区器能够根据您的特定项目更合理地对元素进行分区。(或者只使用小得多的批次。)

这也是假设你事先不严格知道哪些项目是快的,哪些是慢的;如果您这样做了,您可以在调用
ForEach
之前自己重新订购这些物品,以便将昂贵的物品分散开来。这可能是不够的,也可能是不够的,这取决于具体情况


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

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

您可能希望使用库,它有助于设计突出显示并发系统

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

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();
var options=新的ExecutionDataflowBlockOptions{
MaxDegreeOfParallelism=4,
SingleProducerConstrained=true
};
var actionBlock=新的actionBlock(i=>{
WriteLine(“运行索引{0,3}:{1}”,i,DateTime.Now.ToString(“HH:mm:ss.fff”);
系统线程线程睡眠(i<10?1000:10);
},选项);
Task.WhenAll(Enumerable.Range(0,40).Select(actionBlock.SendAsync)).Wait();
actionBlock.Complete();
actionBlock.Completion.Wait();
在此场景中,TPL数据流将使用4个消费者,一旦其中一个消费者可用,就处理一个新值,从而最大化吞吐量


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

使用自定义分区器是修改
Parallel.ForEach()
行为的正确解决方案。如果您使用的是.NET4.5,那么您可以使用。使用它,您的代码将如下所示:

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

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

@kostyan这只是一个简化问题的示例。显然,在读代码中,它正在做一些实际的工作。这根本不是一个罕见的问题。@Vlad当面对这个问题时,人们并不总是事先知道哪些任务快,哪些慢。有时这是一种选择,有时不是。@Servy是的,你是对的。我误解了。我想我的观点是,如果有关于您正在处理的数据的任何信息,您应该充分利用这些信息,并尽可能进行优化。我自己会为每个任务启动一个
任务
,并用
任务创建选项标记长任务。LongRunnin
@Gusdor可能会让系统充斥太多任务,增加开销,并且不应用OP所寻找的并发任务不超过N个的约束。至于应用LongRunningProcess,他可能不知道什么时候开始每个任务,如果该任务将长期运行或不运行,因此这可能是一个问题。我找到了一个能显示我想要的东西。(不知何故,我期望这样一个分区器成为框架的一部分…@erikH,在.NET4.5中。请看我的答案。@svick很高兴知道,尽管我目前仅限于4.0。