首先,要小心Parallel
- 它不能保护您免受线程安全问题的影响。在原始代码中,您在填充结果列表时使用了非线程安全代码。一般来说,您希望避免共享任何状态(尽管在这种情况下对列表的只读访问是可以的)。如果你真的想使用Parallel.For
or Parallel.ForEach
用于过滤和聚合(实际上,AsParallel
是您在这些情况下想要的),您应该使用线程本地状态的重载 - 您将在localFinally
委托(请注意,它仍然在不同的线程上运行,因此您需要确保线程安全;但是,在这种情况下,锁定就可以了,因为您只为每个线程执行一次,而不是在每次迭代时执行此操作)。
现在,解决此类问题显然首先要尝试的是使用探查器。所以我就这么做了。结果如下:
- 这些解决方案中几乎没有任何内存分配。与最初的测试数据分配相比,它们完全相形见绌,即使是相对较小的测试数据(我在测试时使用了 1M、10M 和 100M 的整数)。
- 正在完成的工作是在例如
Parallel.For
or Parallel.ForEach
主体本身,而不是在您的代码中(简单的if (data[i] == 1) results.Add(data[i])
).
第一个意味着我们可以说 GC 可能不是罪魁祸首。确实,它没有任何逃跑的机会。第二个更奇怪 - 这意味着在某些情况下,Parallel
太不合规矩了——但它看起来是随机的,有时它可以顺利工作,有时需要半秒钟。这通常会指向 GC,但我们已经排除了这种可能性。
我尝试过在没有循环状态的情况下使用重载,但这没有帮助。我尝试过限制MaxDegreeOfParallelism
,但它只会伤害事物。现在,显然,这段代码绝对由缓存访问主导——几乎没有任何 CPU 工作,也没有 I/O——这总是有利于单线程解决方案;但即使使用MaxDegreeOfParallelism
1 没有帮助 - 事实上,2 似乎是我的系统上最快的。更多是没有用的——同样,缓存访问占主导地位。它仍然很好奇 - 我使用服务器 CPU 进行测试,它为所有数据一次提供足够的缓存,虽然我们没有进行 100% 顺序访问(这几乎完全消除了延迟) ),它应该足够连续。无论如何,我们在单线程解决方案中拥有内存吞吐量的基线,并且当它运行良好时,它非常接近并行情况的速度(并行,我读到运行时间比单线程少 40%,在四核服务器 CPU 来解决令人尴尬的并行问题 - 显然,内存访问是限制)。
所以,是时候检查一下参考来源了Parallel.For
。在这种情况下,它只是根据工作人员的数量创建范围 - 每个范围。所以这不是范围——没有任何开销。
核心只是运行一个在给定范围内迭代的任务。有一些有趣的地方 - 例如,如果任务花费太长时间,任务将被“暂停”。然而,它似乎不太适合数据 - 为什么这样的事情会导致与数据大小无关的随机延迟?无论工作多么小,无论多么低MaxDegreeOfParallelism
也就是说,我们得到了“随机”的减速。这可能是一个问题,但我不知道如何检查它。
最有趣的是,扩展测试数据对异常没有任何作用 - 虽然它使“好”并行运行得更快(甚至在我的测试中接近完美效率,奇怪的是),“坏”并行运行仍然只是一样糟糕。事实上,在我的一些测试中,它们是absurdly不好(最多是“正常”循环的十倍)。
那么,让我们看一下线程。我人为地增加了线程的数量ThreadPool
确保扩展线程池不是瓶颈(如果一切正常,则不应成为瓶颈,但是......)。第一个惊喜来了 - 虽然“好的”运行只使用有意义的 4-8 个线程,但“坏的”运行会扩展到池中的所有可用线程,即使有一百个线程。哎呀?
让我们再次深入研究源代码。Parallel
内部使用Task.RunSynchronously
运行根分区工作作业,以及Wait
就结果而言。当我查看并行堆栈时,有 97 个线程在执行循环体,而只有一个线程实际上执行了循环体。RunSynchronously
在堆栈上(正如预期的那样 - 这是主线程)。其他是普通的线程池线程。任务 ID 也讲述了一个故事 - 有数千进行迭代时创建的各个任务的数量。显然,有些东西是very这里错了。即使我删除整个循环体,这种情况仍然会发生,所以这也不是什么奇怪的闭包。
显式设置MaxDegreeOfParallelism
在某种程度上抵消了这一点——使用的线程数量不再激增——但是,任务数量仍然如此。但我们已经看到范围只是运行的并行任务的数量 - 那么为什么还要继续创建越来越多的任务呢?使用调试器证实了这一点 - 当 MaxDOP 为 4 时,只有五个范围(有一些对齐导致第五个范围)。有趣的是,其中一个已完成的范围(第一个范围如何比其他范围领先这么多?)有索引higher比它迭代的范围 - 这是因为“调度程序”在最多 16 个切片中分配范围分区。
根任务是自我复制的,因此不必显式启动,例如四个任务来处理数据,它等待调度程序复制任务来处理更多数据。这有点难以阅读 - 我们正在谈论复杂的多线程无锁代码,但似乎它always将工作分配到比分区范围小得多的切片中。在我的测试中,切片的最大大小为 16 - 与我正在运行的数百万数据相差甚远。像这样的主体进行 16 次迭代根本没有时间,这可能会导致算法出现许多问题(最大的问题是基础设施比实际迭代器主体占用更多的 CPU 工作)。在某些情况下,缓存垃圾可能会进一步影响性能(也许当主体运行时有很多变化时),但大多数时候,访问是足够顺序的。
TL; DR
不要使用Parallel.For
and Parallel.ForEach
如果您的每次迭代工作非常短(大约毫秒)。AsParallel
或者仅以单线程运行迭代很可能会快得多。
稍微长一点的解释:
看起来Parallel.For
and Paraller.ForEach
专为您迭代的单个项目需要大量时间来执行的场景而设计(即每个项目有大量工作,而不是大量项目的少量工作)。当迭代器主体太短时,它们似乎表现不佳。如果您没有在迭代器主体中进行大量工作,请使用AsParallel
代替Parallel.*
。最佳点似乎是每个切片 150 毫秒以下(每次迭代大约 10 毫秒)。否则,Parallel.*
会在自己的代码中花费大量时间,而几乎没有时间进行迭代(在我的例子中,通常的数字在主体中大约占 5-10% - 糟糕得令人尴尬)。
遗憾的是,我在 MSDN 上没有找到任何关于此问题的警告 - 甚至有样本检查了大量数据,但没有任何迹象表明这样做会对性能造成严重影响。在我的计算机上测试完全相同的示例代码,我发现它确实通常比单线程迭代慢,而且在最好的情况下,也几乎快不了多少(大约节省 30-40% 的时间)在四个 CPU 核心上运行时- 效率不高)。
EDIT:
Willaien 在 MSDN 上发现了关于这个问题的提及,以及如何解决它 -https://msdn.microsoft.com/en-us/library/dd560853(v=vs.110).aspx https://msdn.microsoft.com/en-us/library/dd560853(v=vs.110).aspx。这个想法是使用自定义分区器并在Parallel.For
主体(例如循环Parallel.For
的循环)。然而,对于大多数情况,使用AsParallel
可能仍然是一个更好的选择 - 简单的循环体通常意味着某种映射/归约操作,并且AsParallel
总的来说,LINQ 非常擅长这一点。例如,您的示例代码可以简单地重写为:
var result = testData.AsParallel().Where(i => i == 1).ToList();
唯一使用的情况AsParallel
与所有其他 LINQ 一样,这是一个坏主意 - 当您的循环体有副作用时。有些可能是可以忍受的,但完全避免它们会更安全。