.Net 中的多个 Parallel.ForEach 循环



在 .Net 进程中,只有一个托管线程池。我们可以根据需要通过公共属性设置最小和最大线程数。

在 .Net 中,我们也有从后台的托管线程池获取线程的Parallel.ForEach

Parallel.ForEach我们还可以设置MaxDegreeOfParallelism以限制最大线程数。

我有两个Parallel.ForEach在平行运行。一个MaxDegreeOfParallelism设置为 3,另一个设置为 7。

我的问题是:我的两个Parallel.ForEach循环是否在引擎盖下使用相同的线程池。如果是,Parallel.ForEach如何使用MaxDegreeOfParallelism限制线程。多倍 循环Parallel.ForEach和一个托管线程池协同工作? 如果你能在我进入 .net 核心源代码之前提供高级解释或一些指针,这将非常有帮助。

  • 我的两个Parallel.ForEach循环是否在引擎盖下使用相同的线程池。

    是的

  • Parallel.ForEach如何使用MaxDegreeOfParallelism限制线程。

    ParallelOptions.MaxDegreeOfParallelism 获取或设置此 ParallelOptions 实例启用的最大并发任务数。

    默认情况下,并行类上的方法尝试使用所有可用的处理器,这些处理器不可取消,并且以默认的任务计划程序 (TaskScheduler.Default) 为目标。并行选项允许覆盖这些默认值。

  • 多个Parallel.ForEach循环和一个托管线程池如何协同工作?

    它们共享同一个线程池。如下所述:

    通常,不需要修改此设置。但是,您可以选择在高级使用方案中显式设置它,例如:

    当您同时运行多个算法并希望手动定义每种算法可以利用的系统量时。您可以为每个值设置最大并行度值。

默认情况下,Parallel.ForEach循环使用来自ThreadPool的线程,这是一个静态类,每个进程只有一个。可以通过配置ParallelOptionsTaskScheduler属性来修改此行为。创建一个用作替代ThreadPool的自定义TaskScheduler并非易事,但也不是火箭科学。可以在此处找到实现。如果您想了解有关自定义任务计划程序的更多信息,可以阅读 Stephen Toub 的这篇文章(代码)。

现在,当两个并行循环同时运行时,会发生什么情况,即两者都在ThreadPool线程上调度工作。如果它们都配置了特定的MaxDegreeOfParallelism,并且两者的总和不超过ThreadPool按需创建的最小线程数¹,则这两个循环不会在调度方面相互干扰。当然,如果这些资源稀缺,仍然可以相互竞争 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

(在小提琴上试试)

您可以看到并行循环 A 最初使用 3 个线程(线程 4、6 和 7),而并行循环 B 仅使用线程 5。此时ThreadPool饱和。大约 500 毫秒后,新的线程 8 被注入,并由 A 循环获取。B 循环仍然只有一个线程。又过了一秒钟,又注入了一个线程,即线程 9。这也适用于循环 A,将比分设置为 5-1 以支持循环 A。这场战斗没有礼貌或礼貌。这是一场对有限资源的激烈竞争。如果希望有多个并行循环并行运行,请确保所有并行循环都配置了其MaxDegreeOfParallelism选项,并且ThreadPool可以按需创建足够的线程来容纳所有这些线程。

¹按方法配置ThreadPool.SetMinThreads,AFAIK 默认等于Environment.ProcessorCount


注意:上面的文本描述了静态Parallel类 (.NET 5) 的现有行为。通过 PLINQ(AsParallelLINQ 运算符)实现的并行性在所有方面的行为并不相同。此外,将来Parallel类可能会获得具有不同默认值的新方法。


.NET 6 更新:上面的示例现在生成不同的输出。比分最终只有 3-2 有利于循环 A:

04:34:47.894 [4] > A-Starting
04:34:47.926 [8] > A-Processing #1
04:34:47.926 [7] > A-Processing #2
04:34:47.926 [4] > A-Processing #3
04:34:48.392 [6] > B-Starting
04:34:48.393 [6] > B-Processing #1
04:34:48.792 [9] > B-Processing #2
04:34:48.927 [4] > A-Processing #4
04:34:48.927 [8] > A-Processing #5
04:34:48.927 [7] > A-Processing #6
04:34:49.393 [6] > B-Processing #3
04:34:49.792 [9] > B-Processing #4
04:34:49.927 [4] > A-Processing #7
04:34:49.927 [8] > A-Processing #8
04:34:49.928 [7] > A-Processing #9
04:34:50.393 [6] > B-Processing #5
04:34:50.792 [9] > B-Processing #6
04:34:50.927 [4] > A-Processing #10
04:34:50.928 [8] > B-Processing #8
04:34:50.928 [7] > B-Processing #7
04:34:51.393 [6] > B-Processing #9
04:34:51.928 [4] > A-Finished
04:34:52.393 [6] > B-Processing #10
04:34:53.394 [6] > B-Finished

注入的线程 9 由循环 B 而不是循环 A 获取。似乎Parallel类的行为,或ThreadPool的行为,或两者兼而有之, 在 .NET 6 中略有变化。但我不确定究竟有什么变化。

最新更新