.Net中的多个并行.ForEach循环

k2fxgqgv  于 2023-03-24  发布在  .NET
关注(0)|答案(2)|浏览(228)

在一个.Net进程中,只有一个托管线程池,我们可以根据需要通过公共属性设置最小和最大线程数。
在.Net中,我们也有Parallel.ForEach,它从后台的托管线程池中获取线程。
Parallel.ForEach中,我们还可以设置MaxDegreeOfParallelism来限制线程的最大数量。
我有两个并行运行的Parallel.ForEach。一个将MaxDegreeOfParallelism设置为3,另一个设置为7。
我的问题是:我的两个Parallel.ForEach循环是否在后台使用相同的线程池。如果是,Parallel.ForEach如何限制MaxDegreeOfParallelism的线程。多个Parallel.ForEach循环和一个托管线程池如何一起工作?如果你能在我进入.net核心源代码之前提供一个高层次的解释或一些指针,那将非常有帮助。

piztneat

piztneat1#

  • 我的两个Parallel.ForEach循环在后台使用相同的线程池吗?

是的

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

MaxDegreeOfParallelism获取或设置this ParallelOptions示例启用的最大并发任务数。
默认情况下,Parallel类上的方法尝试使用所有可用的处理器,是不可取消的,并且以默认的TaskScheduler(TaskScheduler.Default)为目标。

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

它们共享相同的线程池。如下所述:
通常,您不需要修改此设置。但是,您可以选择在以下高级使用方案中显式设置此设置:

当您同时运行多个算法时,需要手动定义每个算法可以占用的系统空间大小,您可以为每个算法设置MaxDegreeOfParallelism值。

r1wp621o

r1wp621o2#

默认情况下,Parallel.ForEach循环使用来自ThreadPool的线程,ThreadPool是一个静态类,只有一个per process。可以通过配置ParallelOptionsTaskScheduler属性来修改此行为。创建一个自定义TaskScheduler作为ThreadPool的替代品并不简单,但也不是火箭科学。可以在这里找到一个实现。如果你想了解更多关于自定义任务调度器的信息,你可以阅读Stephen Toub的this文章(code)。
现在,当两个并行循环同时运行时,会发生什么情况,即两者都在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

Try it on Fiddle
可以看到,并行循环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)的现有行为。通过PLINQAsParallel LINQ操作符)实现的并行性在所有方面都不具有相同的行为。此外,将来Parallel类可能会获得具有不同默认值的新方法。
**.NET 6 update:**上面的例子现在产生了一个不同的输出。分数最终只有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中略有变化。但我不确定具体变化是什么。

相关问题