我应该总是使用Parallel.Foreach,因为更多的线程必须加快一切吗?

Eli*_*eth 42 c# parallel-processing foreach .net-4.0

对于每个正常的foreach使用parallel.foreach循环是否有意义?

我什么时候应该开始使用parallel.foreach,只迭代1,000,000个项目?

Jon*_*eet 69

不,对每个foreach都没有意义.一些原因:

  • 您的代码实际上可能无法并行化.例如,如果您正在使用"到目前为止的结果"进行下一次迭代并且顺序很重要)
  • 如果你正在聚合(例如汇总值),那么有很多方法可以Parallel.ForEach用于此,但你不应该盲目地做
  • 如果你的工作完成得非常快,那就没有任何好处,而且可能会减慢速度

基本上没有任何线程应该盲目地完成.想想并行化实际上有意义的地方.哦,并衡量影响,以确保利益值得增加复杂性.(对于像调试这样的事情来说更难.)TPL很棒,但它不是免费的午餐.


Bri*_*sen 19

不,你绝对不应该这样做.这里重要的不是迭代次数,而是要完成的工作.如果您的工作非常简单,并行执行1000000个委托将增加巨大的开销,并且很可能比传统的单线程解决方案慢.您可以通过对数据进行分区来解决这个问题,因此您可以执行大量工作.

例如,考虑以下情况:

Input = Enumerable.Range(1, Count).ToArray();
Result = new double[Count];

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

这里的操作非常简单,并行执行此操作的开销会使使用多个内核的收益相形见绌.此代码运行速度明显慢于常规foreach循环.

通过使用分区,我们可以减少开销并实际观察到性能的提升.

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

这里故事的士气是并行性很难,你应该在仔细观察眼前的情况后才能使用它.此外,您应该在添加并行性之前和之后分析代码.

请记住,无论任何潜在的性能增益,并行性总是会增加代码的复杂性,因此如果性能已经足够好,则没有理由增加复杂性.

  • "如果您的工作非常简单,并行执行1000000个代理将增加巨大的开销,并且很可能比传统的单线程解决方案慢." 这是错误的,我将发布显示此基准的基准.有一个"最大并行度"的概念和Parallel.ForEach将尝试决定应该是什么.它不会尝试"并行执行1000000个代表".它并不总能做出最好的决定,但它几乎总是比非线程更好.分区是个好主意,您可以显式设置MaxDegreeOfParallelism. (8认同)
  • 我用 1000 到 50 000 个项目的数据对其进行了测试。在每种情况下,parallel.foreach 都比没有时更快(1.3-2 倍)。每次我尝试通过限制使用的最大线程数或使用不同的分区器来优化它时,它要么一样快,要么变得更慢。Parallel.ForEach 的默认优化似乎效果很好。 (2认同)

Ant*_*ony 13

简短的回答是否定的,你不应该只Parallel.ForEach在每个循环上使用或相关的结构.并行有一些开销,这在具有很少,快速迭代的循环中是不合理的.而且,break这些循环内部要复杂得多.

Parallel.ForEach是调度循环作为任务调度器认为合适的,基于在所述循环迭代次数,在硬件和电流负载在该硬件上的CPU内核的数量的请求.实际并行执行并不总是得到保证,并且如果内核较少,迭代次数较少和/或当前负载较高,则不太可能.

另请参见Parallel.ForEach是否限制活动线程的数量?Parallel.For每次迭代使用一个任务?

答案很长:

我们可以根据它们落在两个轴上的方式对循环进行分类:

  1. 很少迭代到很多次迭代.
  2. 每次迭代都是快速通过,每次迭代都很慢.

第三个因素是,如果任务持续时间变化很大 - 例如,如果你正在计算在Mandelbrot集点,一些点是快速计算,一些需要更长的时间.

当很少,快速迭代时,可能不值得以任何方式使用并行化,最有可能由于开销而最终会变慢.即使并行化确实加速了特定的小型快速循环,也不太可能引起人们的兴趣:增益很小,并且它不是应用程序中的性能瓶颈,因此优化可读性而不是性能.

如果一个循环只有非常少的,慢的迭代并且你想要更多的控制,你可以考虑使用Tasks来处理它们,类似于:

var tasks = new List<Task>(actions.Length); 
foreach(var action in actions) 
{ 
    tasks.Add(Task.Factory.StartNew(action)); 
} 
Task.WaitAll(tasks.ToArray());
Run Code Online (Sandbox Code Playgroud)

有很多次迭代的地方Parallel.ForEach就是它的元素.

微软的文档指出,

当并行循环运行时,TPL对数据源进行分区,以便循环可以同时在多个部分上运行.在幕后,任务计划程序根据系统资源和工作负载对任务进行分区.如果可能,调度程序会在工作负载变得不平衡时在多个线程和处理器之间重新分配工作.

随着循环迭代次数的减少,这种分区和动态重新调度将更难以有效地完成,并且如果迭代在持续时间和同一台机器上运行的其他任务的存在下变化,则更加必要.

我运行了一些代码.

下面的测试结果显示一台机器上没有运行其他任何东西,并且没有使用其他来自.Net线程池的线程.这不是典型的(实际上在Web服务器场景中,这是非常不现实的).实际上,您可能看不到任何具有少量迭代的并行化.

测试代码是:

namespace ParallelTests 
{ 
    class Program 
    { 
        private static int Fibonacci(int x) 
        { 
            if (x <= 1) 
            { 
                return 1; 
            } 
            return Fibonacci(x - 1) + Fibonacci(x - 2); 
        } 

        private static void DummyWork() 
        { 
            var result = Fibonacci(10); 
            // inspect the result so it is no optimised away. 
            // We know that the exception is never thrown. The compiler does not. 
            if (result > 300) 
            { 
                throw new Exception("failed to to it"); 
            } 
        } 

        private const int TotalWorkItems = 2000000; 

        private static void SerialWork(int outerWorkItems) 
        { 
            int innerLoopLimit = TotalWorkItems / outerWorkItems; 
            for (int index1 = 0; index1 < outerWorkItems; index1++) 
            { 
                InnerLoop(innerLoopLimit); 
            } 
        } 

        private static void InnerLoop(int innerLoopLimit) 
        { 
            for (int index2 = 0; index2 < innerLoopLimit; index2++) 
            { 
                DummyWork(); 
            } 
        } 

        private static void ParallelWork(int outerWorkItems) 
        { 
            int innerLoopLimit = TotalWorkItems / outerWorkItems; 
            var outerRange = Enumerable.Range(0, outerWorkItems); 
            Parallel.ForEach(outerRange, index1 => 
            { 
                InnerLoop(innerLoopLimit); 
            }); 
        } 

        private static void TimeOperation(string desc, Action operation) 
        { 
            Stopwatch timer = new Stopwatch(); 
            timer.Start(); 
            operation(); 
            timer.Stop(); 

            string message = string.Format("{0} took {1:mm}:{1:ss}.{1:ff}", desc, timer.Elapsed); 
            Console.WriteLine(message); 
        } 

        static void Main(string[] args) 
        { 
            TimeOperation("serial work: 1", () => Program.SerialWork(1)); 
            TimeOperation("serial work: 2", () => Program.SerialWork(2)); 
            TimeOperation("serial work: 3", () => Program.SerialWork(3)); 
            TimeOperation("serial work: 4", () => Program.SerialWork(4)); 
            TimeOperation("serial work: 8", () => Program.SerialWork(8)); 
            TimeOperation("serial work: 16", () => Program.SerialWork(16)); 
            TimeOperation("serial work: 32", () => Program.SerialWork(32)); 
            TimeOperation("serial work: 1k", () => Program.SerialWork(1000)); 
            TimeOperation("serial work: 10k", () => Program.SerialWork(10000)); 
            TimeOperation("serial work: 100k", () => Program.SerialWork(100000)); 

            TimeOperation("parallel work: 1", () => Program.ParallelWork(1)); 
            TimeOperation("parallel work: 2", () => Program.ParallelWork(2)); 
            TimeOperation("parallel work: 3", () => Program.ParallelWork(3)); 
            TimeOperation("parallel work: 4", () => Program.ParallelWork(4)); 
            TimeOperation("parallel work: 8", () => Program.ParallelWork(8)); 
            TimeOperation("parallel work: 16", () => Program.ParallelWork(16)); 
            TimeOperation("parallel work: 32", () => Program.ParallelWork(32)); 
            TimeOperation("parallel work: 64", () => Program.ParallelWork(64)); 
            TimeOperation("parallel work: 1k", () => Program.ParallelWork(1000)); 
            TimeOperation("parallel work: 10k", () => Program.ParallelWork(10000)); 
            TimeOperation("parallel work: 100k", () => Program.ParallelWork(100000)); 

            Console.WriteLine("done"); 
            Console.ReadLine(); 
        } 
    } 
} 
Run Code Online (Sandbox Code Playgroud)

在4核Windows 7机器上的结果是:

serial work: 1 took 00:02.31 
serial work: 2 took 00:02.27 
serial work: 3 took 00:02.28 
serial work: 4 took 00:02.28 
serial work: 8 took 00:02.28 
serial work: 16 took 00:02.27 
serial work: 32 took 00:02.27 
serial work: 1k took 00:02.27 
serial work: 10k took 00:02.28 
serial work: 100k took 00:02.28 

parallel work: 1 took 00:02.33 
parallel work: 2 took 00:01.14 
parallel work: 3 took 00:00.96 
parallel work: 4 took 00:00.78 
parallel work: 8 took 00:00.84 
parallel work: 16 took 00:00.86 
parallel work: 32 took 00:00.82 
parallel work: 64 took 00:00.80 
parallel work: 1k took 00:00.77 
parallel work: 10k took 00:00.78 
parallel work: 100k took 00:00.77 
done
Run Code Online (Sandbox Code Playgroud)

运行代码在.Net 4和.Net 4.5中编译得到的结果大致相同.

连续工作运行完全相同.如何切片它并不重要,它运行大约2.28秒.

1次迭代的并行工作比没有并行性略长.2个项目更短,因此是3个,并且4次或更多次迭代都是大约0.8秒.

它使用所有核心,但不是100%效率.如果串行工作分为4种方式而没有开销,则会在0.57秒内完成(2.28/4 = 0.57).

在其他情况下,我看到并行2-3次迭代完全没有加速.你不必在这细粒度的控制与Parallel.ForEach和算法可以决定"分区"他们逼到了1块和1个核心运行它如果机器正忙.


Gab*_*abe 9

进行并行操作没有下限.如果您只有2个项目可以使用,但每个项目都需要一段时间,那么使用它可能仍然有意义Parallel.ForEach.另一方面,如果你有1000000个项目,但它们做得不多,那么并行循环可能不会比常规循环更快.

例如,我编写了一个简单的程序来计算嵌套循环的时间,其中外部循环运行for循环和使用Parallel.ForEach.我把它放在我的4-CPU(双核,超线程)笔记本电脑上.

这是一个只有2个项目可以运行的运行,但每个都需要一段时间:

2 outer iterations, 100000000 inner iterations:
for loop: 00:00:00.1460441
ForEach : 00:00:00.0842240

这是一个包含数百万个项目的运行,但它们并没有做很多事情:

100000000 outer iterations, 2 inner iterations:
for loop: 00:00:00.0866330
ForEach : 00:00:02.1303315

要知道的唯一真正的方法是尝试它.

  • @Anthony:请参阅我的编辑,了解以下情况:使用“Parallel.ForEach”进行 2 个长操作会更快,但使用“for”进行 100000000 个简单操作会更快。 (3认同)