对于给定的
MaxDegreeOfParallelism
和需要处理的固定数量的对象(即在它们上执行某些代码),似乎 Parallel.ForEach
和 ActionBlock
同样有用。
选择其中之一时需要考虑哪些因素?
Parallel.ForEach
和ActionBlock<T>
都可以用于并行处理项目列表。在这两者之间,Parallel.ForEach
是更自然的选择,因为它清楚地传达了其目的,并且在使用之前需要较少的研究。两者都有可能让您大吃一惊的问题。以下是一些应该记住的事情:
Parallel.ForEach
处理项目的顺序取决于 source
的类型。如果它是列表或数组,则顺序将非常特殊,因为 Parallel.ForEach
会将列表按范围进行分区,并为每个范围分配一个工作任务(范围分区)。所以你会看到要处理的项目如下:1, 26, 51, 76, 2, 27, 52, 77...,而不是自然的 1, 2, 3, 4, 5, 6, 7, 8 等。如果源是 IEnumerable<T>
,则顺序将是正常的开始到结束。 ActionBlock<T>
按照您 Post
的顺序处理项目,因此不会出现任何意外。
当
source
是 IEnumerable<T>
时,Parallel.ForEach
默认使用 chunk 分区,这意味着它一次不会仅从 source
中获取一项。它将项目累积成小块,然后开始处理它们。例如,如果您的 source
是 BlockingCollection<T>
,这可能会让您感到惊讶。您将在集合中添加一个项目,但 Parallel.ForEach
不会立即处理它,您会想知道为什么。 ActionBlock<T>
将项目逐一放入自己的缓冲区中,因此不会有任何意外。
默认情况下,
ActionBlock<T>
有MaxDegreeOfParallelism = 1
(即没有并行性)。相反,默认情况下 Parallel.ForEach
具有 MaxDegreeOfParallelism = -1
(即无限并行度)。 Parallel.ForEach
具有迄今为止 最危险的默认设置,因为如果您忘记配置 MaxDegreeOfParallelism
,它将很快使您的 ThreadPool
饱和。当 ThreadPool
饱和时,程序的其他并发操作将会卡顿。
ActionBlock<T>
具有令人讨厌的“设计”行为,会吞下OperationCancelledException
抛出的任何action
。因此,如果处理某个项目可能会因 OperationCancelledException
失败,即,如果在您的情况下此异常表示失败而不是取消,则 ActionBlock<T>
将愉快地完成,没有任何异常,就像什么都没发生一样,向您隐藏处理实际上失败的情况。
我之前提到的
Parallel.ForEach
的特殊性可以通过将 source
包装在适当的 Partitioner
中来轻松修复,如这个答案所示。他们还可以通过切换到更新的 Parallel.ForEachAsync
API 来进行更彻底的修复。尽管 Parallel.ForEachAsync
的名称中有 Async
,但它可以同样轻松高效地处理同步工作负载。只需从 ValueTask.CompletedTask
返回一个 body
,然后 Wait
得到结果 Task
。虽然没有记录,但Parallel.ForEachAsync
采用的分块/分区策略并不令人惊讶。它按照自然的开始到结束顺序处理项目。它对于拥有 ThreadPool
也不太积极,因为默认情况下它的 MaxDegreeOfParallelism
等于 Environment.ProcessorCount
,这对于大多数情况来说是一个合理的默认值。当它的工作任务从 source
获取项目时,它是 异步同步的,因此如果源是空的
BlockingCollection<T>
则只有一个线程会被阻塞。它缺少 Parallel.ForEach
所具有的一些功能,例如破坏和获取 LowestBreakIteration
,但这些功能在实践中很少使用。