.NET 6 中的并行操作中的 MaxDegreeOfParallelism = -1 是什么意思?

What is the meaning of the MaxDegreeOfParallelism = -1 in Parallel operations in .NET 6?

ParallelOptions.MaxDegreeOfParallelism 属性 的文档指出:

The MaxDegreeOfParallelism property affects the number of concurrent operations run by Parallel method calls that are passed this ParallelOptions instance. A positive property value limits the number of concurrent operations to the set value. If it is -1, there is no limit on the number of concurrently running operations.

By default, For and ForEach will utilize however many threads the underlying scheduler provides, so changing MaxDegreeOfParallelism from the default only limits how many concurrent tasks will be used.

我想了解 “无限制” 在这种情况下的含义。根据文档的上述摘录,我的期望是 MaxDegreeOfParallelism = -1 配置的 Parallel.Invoke 操作将立即开始并行执行 all 提供的 actions.但这不是正在发生的事情。这是一个包含 12 个动作的实验:

int concurrency = 0;
Action action = new Action(() =>
{
    var current = Interlocked.Increment(ref concurrency);
    Console.WriteLine(@$"Started an action at {DateTime
        .Now:HH:mm:ss.fff} on thread #{Thread
        .CurrentThread.ManagedThreadId} with concurrency {current}");
    Thread.Sleep(1000);
    Interlocked.Decrement(ref concurrency);
});
Action[] actions = Enumerable.Repeat(action, 12).ToArray();
var options = new ParallelOptions() { MaxDegreeOfParallelism = -1 };
Parallel.Invoke(options, actions);

输出:

Started an action at 11:04:42.636 on thread #6 with concurrency 4
Started an action at 11:04:42.636 on thread #7 with concurrency 5
Started an action at 11:04:42.629 on thread #1 with concurrency 1
Started an action at 11:04:42.636 on thread #8 with concurrency 3
Started an action at 11:04:42.630 on thread #4 with concurrency 2
Started an action at 11:04:43.629 on thread #9 with concurrency 6
Started an action at 11:04:43.648 on thread #6 with concurrency 6
Started an action at 11:04:43.648 on thread #8 with concurrency 6
Started an action at 11:04:43.648 on thread #4 with concurrency 6
Started an action at 11:04:43.648 on thread #7 with concurrency 6
Started an action at 11:04:43.648 on thread #1 with concurrency 6
Started an action at 11:04:44.629 on thread #9 with concurrency 6

(Live demo)

这个实验的结果与我的预期不符。并非所有操作都被立即调用。记录的最大并发是6,有时是7,但不是12。所以“无限制”并不是我想的意思。我的问题是:MaxDegreeOfParallelism = -1 配置意味着什么 完全 ,所有四个 Parallel methods (For, ForEach, ForEachAsync and Invoke)? I want to know in details what's the behavior of these methods, when configured this way. In case there are behavioral differences between .NET versions, I am interested about the current .NET version (.NET 6), which also introduced the new Parallel.ForEachAsync API.

第二个问题:MaxDegreeOfParallelism = -1 与在这些方法中省略可选的 parallelOptions 参数是否完全相同?


澄清: 我对 configured 默认 TaskSchedulerParallel 方法的行为很感兴趣。我对使用专门或自定义调度程序可能出现的任何并发症感兴趣。

定义故意写成-1 means that the number of number of concurrent operations will not be artificially limited.,并没有说所有的动作都会立即开始。

线程池管理器通常将可用线程数保持在内核数(或逻辑处理器是内核数的 2 倍),这被认为是最佳线程数 (我认为这number 是 [number of cores/logical processor + 1]) 。这意味着当您开始执行操作时,立即开始工作的可用线程数就是这个数字。

线程池管理器 运行s 周期性地(每秒两次)并且如果 none 的线程已经完成,则添加一个新的线程(或者在相反的情况下删除线程太多线程)。

一个很好的实验来观察这个在行动中也是如此 运行 你的实验快速连续两次。在第一个实例中,开始时并发作业的数量应该是 cores/logical 处理器的数量 + 1,而在第二个 运行 中,它应该是作业的数量 运行 (因为这些线程是创建服务第一个 运行:

这是您的代码的修改版本:

using System.Diagnostics;

Stopwatch sw = Stopwatch.StartNew();
int concurrency = 0;
Action action = new Action(() =>
{
    var current = Interlocked.Increment(ref concurrency);
    Console.WriteLine(@$"Started at {sw.ElapsedMilliseconds} with concurrency {current}");
    Thread.Sleep(10_000);
    current = Interlocked.Decrement(ref concurrency);
});


Action[] actions = Enumerable.Repeat(action, 12).ToArray();
var options = new ParallelOptions() { MaxDegreeOfParallelism = -1 };
Parallel.Invoke(options, actions);

Parallel.Invoke(options, actions);

输出:

Started at 114 with concurrency 8
Started at 114 with concurrency 1
Started at 114 with concurrency 2
Started at 114 with concurrency 3
Started at 114 with concurrency 4
Started at 114 with concurrency 6
Started at 114 with concurrency 5
Started at 114 with concurrency 7
Started at 114 with concurrency 9
Started at 1100 with concurrency 10
Started at 2097 with concurrency 11
Started at 3100 with concurrency 12
Started at 13110 with concurrency 1
Started at 13110 with concurrency 2
Started at 13110 with concurrency 3
Started at 13110 with concurrency 5
Started at 13110 with concurrency 7
Started at 13110 with concurrency 9
Started at 13110 with concurrency 10
Started at 13110 with concurrency 11
Started at 13110 with concurrency 4
Started at 13110 with concurrency 12
Started at 13110 with concurrency 6
Started at 13110 with concurrency 8

我的计算机有 4 个核心(8 个逻辑处理器),当作业 运行 在“冷” TaskScheduler.Default 上时,我们首先立即启动其中的 8+1 个,然后是一个新的定期添加线程。

然后,当 运行第二批“热”时,所有作业同时开始。

Parallel.ForEachAsync

当类似示例是 运行 和 Parallel.ForEachAsync 时,行为是不同的。这项工作是在恒定的并行水平下完成的。请注意,这与线程无关,因为如果您 await Task.Delay(因此不会阻塞线程),并行作业的数量将保持不变。

如果我们查看采用 ParallelOptions 版本的源代码,它会将 parallelOptions.EffectiveMaxConcurrencyLevel 作为 dop 传递给执行实际工作的私有方法。

public static Task ForEachAsync<TSource>(IEnumerable<TSource> source!!, ParallelOptions parallelOptions!!, Func<TSource, CancellationToken, ValueTask> body!!)
{
     return ForEachAsync(source, parallelOptions.EffectiveMaxConcurrencyLevel, ...);
}

如果我们进一步观察,我们可以看到:

  • “dop”记录为“一个整数,表示允许运行并行执行多少个操作。”。
  • 实际并行度为 DefaultDegreeOfParallelism
/// <param name="dop">A integer indicating how many operations to allow to run in parallel.</param>
(...)
private static Task ForEachAsync<TSource>(IEnumerable<TSource> source, int dop,
{
    ...

    if (dop < 0)
    {
        dop = DefaultDegreeOfParallelism;
    }

最后看一眼,我们可以看到最终值为 Environment.ProcessorCount

private static int DefaultDegreeOfParallelism => Environment.ProcessorCount;

这是现在的样子,我不确定在 .NET 7 中是否会保持这样。