ParallelOptions.MaxDegreeOfParallelism
is not global, it is per parallel loop. And more specifically, it sets the max number of tasks that can run in parallel, not the max number of cores or threads that will run those tasks in parallel.
Some demo tests
note: i have 4 cores, 8 threads
What's happening in the code
- We're running 2 async methods; each one kicks off nested parallel loops.
- We're setting max degrees of parallelism to 2 and a sleep time of 2 seconds to simulate the work each task does
- So, due to setting
MaxDegreeOfParallelism
to 2, we would expect to reach up to 12 concurrent tasks before the 40 tasks complete (i'm only counting tasks kicked off by the nested parallel loops)
- how do i get 12?
- 2 max concurrent tasks started in the outer loop
- +4 max concurrent tasks from inner loop (2 started per task started in outer loop)
- that's 6 (per asynchronous task kicked off in
Main
)
- 12 total
test code
using System;
using System.Threading;
using System.Threading.Tasks;
namespace forfun
{
class Program
{
static void Main(string[] args)
{
var taskRunner = new TaskRunner();
taskRunner.RunTheseTasks();
taskRunner.RunTheseTasksToo();
Console.ReadLine();
}
private class TaskRunner
{
private int _totalTasks = 0;
private int _runningTasks = 0;
public async void RunTheseTasks()
{
await Task.Run(() => ProcessThingsInParallel());
}
public async void RunTheseTasksToo()
{
await Task.Run(() => ProcessThingsInParallel());
}
private void ProcessThingsInParallel()
{
ParallelOptions po = new ParallelOptions();
po.MaxDegreeOfParallelism = 2;
Parallel.For(0, 4, po, (i) =>
{
Interlocked.Increment(ref _totalTasks);
Interlocked.Increment(ref _runningTasks);
Console.WriteLine($"{_runningTasks} currently running of {_totalTasks} total tasks");
Parallel.For(0, 4, po, (j) =>
{
Interlocked.Increment(ref _totalTasks);
Interlocked.Increment(ref _runningTasks);
Console.WriteLine($"{_runningTasks} currently running of {_totalTasks} total tasks");
WorkMethod(i, j); // assume a long-running method
Interlocked.Decrement(ref _runningTasks);
});
Interlocked.Decrement(ref _runningTasks);
}
);
}
private static void WorkMethod(int i, int l)
{
Thread.Sleep(2000);
}
}
}
}
Spoiler, the output shows that setting MaxDegreeOfParallelism
is not global, is not limited to core or thread count, and is specifically setting a max on concurrent running tasks.
output with max set to 2:
1 currently running of 1 total tasks
3 currently running of 3 total tasks
2 currently running of 2 total tasks
4 currently running of 4 total tasks
5 currently running of 5 total tasks
7 currently running of 7 total tasks
[ ... snip ...]
11 currently running of 33 total tasks
12 currently running of 34 total tasks
11 currently running of 35 total tasks
12 currently running of 36 total tasks
11 currently running of 37 total tasks
12 currently running of 38 total tasks
11 currently running of 39 total tasks
12 currently running of 40 total tasks
(output will vary, but each time, the max concurrent should be 12)
output without max set:
1 currently running of 1 total tasks
3 currently running of 3 total tasks
4 currently running of 4 total tasks
2 currently running of 2 total tasks
5 currently running of 5 total tasks
7 currently running of 7 total tasks
[ ... snip ...]
19 currently running of 28 total tasks
19 currently running of 29 total tasks
18 currently running of 30 total tasks
13 currently running of 31 total tasks
13 currently running of 32 total tasks
16 currently running of 35 total tasks
16 currently running of 36 total tasks
14 currently running of 33 total tasks
15 currently running of 34 total tasks
15 currently running of 37 total tasks
16 currently running of 38 total tasks
16 currently running of 39 total tasks
17 currently running of 40 total tasks
notice how without setting the max, we get up to 19 concurrent tasks
- now the 2 second sleep time is limiting the number of tasks that could kick off before others finished
output after increasing sleep time to 12 seconds
1 currently running of 1 total tasks
2 currently running of 2 total tasks
3 currently running of 3 total tasks
4 currently running of 4 total tasks
[ ... snip ...]
26 currently running of 34 total tasks
26 currently running of 35 total tasks
27 currently running of 36 total tasks
28 currently running of 37 total tasks
28 currently running of 38 total tasks
28 currently running of 39 total tasks
28 currently running of 40 total tasks
got up to 28 concurrent tasks
now setting loops to 10 nested in 10 and setting sleep time back to 2 seconds - again no max set
1 currently running of 1 total tasks
3 currently running of 3 total tasks
2 currently running of 2 total tasks
4 currently running of 4 total tasks
[ ... snip ...]
38 currently running of 176 total tasks
38 currently running of 177 total tasks
38 currently running of 178 total tasks
37 currently running of 179 total tasks
38 currently running of 180 total tasks
38 currently running of 181 total tasks
[ ... snip ...]
35 currently running of 216 total tasks
35 currently running of 217 total tasks
32 currently running of 218 total tasks
32 currently running of 219 total tasks
33 currently running of 220 total tasks
got up to 38 concurrent tasks before all 220 finished
More related information
ParallelOptions.MaxDegreeOfParallelism Property
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.
to get the max degree of parallelism, don't set it, rather allow the TPL and its scheduler handle it
setting the max degree of parallelism only affects the number of concurrent tasks, not threads used
the maximum number of concurrent tasks is not equal to the number of threads available--threads will still be able to juggle multiple tasks; and even if your app is using all threads, it is still sharing those threads with the other processes that the machine is hosting
Environment.ProcessorCount
Gets the number of processors on the current machine.
What if we say MaxDegreeOfParallelism = Environment.ProcessorCount
?
Even setting max degree of parallism to Environment.ProcessorCount
does not dynamically ensure that you get the maximum concurrency regardless of the system your app is running on. Doing this still limits the degree of parallelism, because any given thread can switch between many tasks--so this would just limit the number of concurrent tasks to equal the number of available threads--and this does not necessarily mean that each concurrent task will be assigned neatly to each thread in a one-to-one relationship.