The documentation of the ParallelOptions.MaxDegreeOfParallelism property states that:
The
MaxDegreeOfParallelismproperty affects the number of concurrent operations run byParallelmethod calls that are passed thisParallelOptionsinstance. 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,
ForandForEachwill utilize however many threads the underlying scheduler provides, so changingMaxDegreeOfParallelismfrom the default only limits how many concurrent tasks will be used.
I am trying to understand what "no limit" means in this context. Based on the above excerpt from the docs, my expectation was that a Parallel.Invoke operation configured with MaxDegreeOfParallelism = -1 would start executing immediately in parallel all the supplied actions. But this is not what happening. Here is an experiment with 12 actions:
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);
Output:
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
The result of this experiment does not match my expectations. Not all actions were invoked immediately. The maximum concurrency recorded is 6, and sometimes 7, but not 12. So the "no limit" does not mean what I think it means. My question is: what does the MaxDegreeOfParallelism = -1 configuration means exactly, with all four 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.
Secondary question: Is the MaxDegreeOfParallelism = -1 exactly the same with omitting the optional parallelOptions argument in these methods?
Clarification: I am interested about the behavior of the Parallel methods when configured with the default TaskScheduler. I am not interested about any complications that might arise by using specialized or custom schedulers.
The definition is deliberately states as
-1 means that the number of number of concurrent operations will not be artificially limited.and it doesn't say that all actions will start immediately.The thread pool manager normally keeps the number of available threads at the number of cores (or logical processor which are 2x number of cores) and this is considered the optimum number of threads (I think this number is [number of cores/logical processor + 1]) . This means that when you start executing your actions the number of available threads to immediately start work is this number.
Thread pool manager runs periodically (twice a second) and if none of the threads have completed a new one is added (or removed in the reverse situation when there are too many threads).
A good experiment to see this in action is to run your experiment twice in quick succession. In the first instance the number of concurrent jobs at the beginning should be around the number of cores/logical processor + 1 and in the 2nd run it should be the number of jobs run (because these threads were created to service the 1st run):
Here's a modified version of your code:
Output:
My computer has 4 cores (8 logical processors) and when the jobs run on a "cold"
TaskScheduler.Defaultat first 8+1 of them are started immediately and after that a new thread is added periodically.Then, when running the second batch "hot" then all jobs start at the same time.
Parallel.ForEachAsync
When a similar example is run with
Parallel.ForEachAsyncthe behaviour is different. The work is done at a constant level of paralellism. Please note that this is not about threads because if youawait Task.Delay(so not block the thread) the number of parallel jobs stays the same.If we peek at the source code for the version taking
ParallelOptionsit passesparallelOptions.EffectiveMaxConcurrencyLevelasdopto the private method which does the real work.If we peek further we can see that:
DefaultDegreeOfParallelism.One last peek, and we can see the final value is
Environment.ProcessorCount.This is what it is now and I am not sure if this will stay like this in .NET 7.