默认情况下,Parallel.ForEach 循环使用来自ThreadPool 的线程,这是一个静态类,它只有一个per process。可以通过配置ParallelOptions 的TaskScheduler 属性来修改此行为。创建一个自定义的TaskScheduler 作为替代ThreadPool 并不是一件容易的事,但也不是火箭科学。如果您有兴趣,可以找到here 一些可以帮助您入门的材料(article)。
现在当两个并行循环同时运行时会发生什么,它们都在ThreadPool 线程上调度工作。如果它们都配置了特定的MaxDegreeOfParallelism,并且两者的总和不超过ThreadPool 按需创建的最小线程数¹,那么这两个循环不会在调度方面相互干扰。当然,在 CPU 资源稀缺的情况下,仍然可以相互竞争 CPU 资源。在这种情况下,操作系统将成为仲裁者。
如果至少有一个并行循环没有配置特定的MaxDegreeOfParallelism,则此选项的有效默认值为-1,这意味着无限并行。这将导致ThreadPool 立即饱和,并保持饱和直到未配置并行循环的源可枚举完成。在此期间,两个并行循环将相互严重干扰,谁将获得饱和ThreadPool 每约 1,000 毫秒注入的额外线程取决于谁先要求它。最重要的是,饱和的ThreadPool 会对在此期间也可能处于活动状态的任何其他独立回调、计时器事件、异步延续等产生负面影响。
如果两个并行循环都配置了,并且两者的总和MaxDegreeOfParallelism超过了可用线程的数量,那么情况与之前类似。唯一的区别是ThreadPool中的线程数会逐渐增加,饱和事件可能会比并行循环的执行更早结束。
以下是演示此行为的示例:
ThreadPool.SetMinThreads(4, 4);
Task[] tasks = new[] { 'A', 'B' }.Select(name => Task.Run(() =>
{
Thread.Sleep(100); if (name == 'B') Thread.Sleep(500);
Print($"{name}-Starting");
var options = new ParallelOptions() { MaxDegreeOfParallelism = 10 };
Parallel.ForEach(Enumerable.Range(1, 10), options, item =>
{
Print($"{name}-Processing #{item}");
Thread.Sleep(1000);
});
Print($"{name}-Finished");
})).ToArray();
Task.WaitAll(tasks);
static void Print(string line)
{
Console.WriteLine($@"{DateTime.Now:HH:mm:ss.fff} [{Thread.CurrentThread
.ManagedThreadId}] > {line}");
}
输出:
15:34:20.054 [4] > A-Starting
15:34:20.133 [6] > A-Processing #2
15:34:20.133 [7] > A-Processing #3
15:34:20.133 [4] > A-Processing #1
15:34:20.552 [5] > B-Starting
15:34:20.553 [5] > B-Processing #1
15:34:20.956 [8] > A-Processing #4
15:34:21.133 [4] > A-Processing #5
15:34:21.133 [7] > A-Processing #6
15:34:21.133 [6] > A-Processing #7
15:34:21.553 [5] > B-Processing #2
15:34:21.957 [8] > A-Processing #8
15:34:21.957 [9] > A-Processing #9
15:34:22.133 [4] > A-Processing #10
15:34:22.134 [7] > B-Processing #3
15:34:22.134 [6] > B-Processing #4
15:34:22.553 [5] > B-Processing #5
15:34:22.957 [8] > B-Processing #6
15:34:22.958 [9] > B-Processing #7
15:34:23.134 [4] > A-Finished
15:34:23.134 [4] > B-Processing #8
15:34:23.135 [7] > B-Processing #9
15:34:23.135 [6] > B-Processing #10
15:34:24.135 [5] > B-Finished
(Try it on Fiddle)
您可以看到并行循环 A 最初使用 3 个线程(线程 4、6 和 7),而并行循环 B 仅使用线程 5。此时 ThreadPool 已饱和。大约 500 毫秒后,新线程 8 被注入,并被 A 循环占用。 B 循环仍然只有一个线程。又过了一秒,又注入了一个线程,线程 9。这也适用于循环A,将比分定为5-1,有利于循环A。这场战斗没有礼貌或礼貌。这是对有限资源的激烈竞争。如果您希望有多个并行循环并行运行,请确保所有循环都配置了MaxDegreeOfParallelism 选项,并且ThreadPool 可以按需创建足够的线程来容纳所有线程。
注意:以上文字描述了静态Parallel 类(.NET 5)的现有行为。通过PLINQ(AsParallel LINQ 运算符)实现的并行性在所有方面都有不同的行为。同样在未来Parallel 类可能会获得具有不同默认值的新方法。
¹ 通过ThreadPool.SetMinThreads方法配置,AFAIK默认等于Environment.ProcessorCount。