6 KiB
Using multiple threads with parallel LINQ
This is an optional bonus section for Chapter 11. It is not required to complete the rest of the book.
By default, only one thread is used to execute a LINQ query. Parallel LINQ (PLINQ) is an easy way to enable multiple threads to execute a LINQ query.
Good Practice: Do not assume that using parallel threads will improve the performance of your applications. Always measure real-world timings and resource usage.
Creating an app that benefits from multiple threads
To see it in action, we will start with some code that only uses a single thread to calculate Fibonacci numbers for 45 integers. We will use the StopWatch type to measure the change in performance.
We will use operating system tools to monitor the CPU and CPU core usage. If you do not have multiple CPUs or at least multiple cores, then this exercise won't show much!
- Use your preferred code editor to add a new Console App /
consoleproject namedLinqInParallelto theChapter11solution/workspace.- In Visual Studio Code, select
LinqInParallelas the active OmniSharp project.
- In Visual Studio Code, select
- In
Program.cs, delete the existing statements and then import theSystem.Diagnosticsnamespace so that we can use theStopWatchtype. Add statements to create a stopwatch to record timings, wait for a keypress before starting the timer, create 45 integers, calculate the Fibonacci number for each of them, stop the timer, and display the elapsed milliseconds, as shown in the following code:
using System.Diagnostics; // Stopwatch
Write("Press ENTER to start. "); ReadLine();
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
int max = 45;
IEnumerable<int> numbers = Enumerable.Range(start: 1, count: max);
WriteLine($"Calculating Fibonacci sequence up to term {max}. Please wait...");
int[] fibonacciNumbers = numbers
.Select(number => Fibonacci(number))
.ToArray();
watch.Stop();
WriteLine("{0:#,##0} elapsed milliseconds.",
arg0: watch.ElapsedMilliseconds);
Write("Results:");
foreach (int number in fibonacciNumbers)
{
Write($" {number:N0}");
}
static int Fibonacci(int term) =>
term switch
{
1 => 0,
2 => 1,
_ => Fibonacci(term - 1) + Fibonacci(term - 2)
};
- Run the console app, but do not press Enter to start the stopwatch yet because we need to make sure a monitoring tool is showing processor activity.
Using Windows
If you are using Windows:
- Right-click on the Windows Start button or press Ctrl + Alt + Delete, and then click on Task Manager.
- At the bottom of the Task Manager window, click More details.
- At the top of the Task Manager window, click on the Performance tab.
- Right-click on the CPU Utilization graph, select Change graph to, and then select Logical processors.
Using macOS
If you are using macOS:
- Launch Activity Monitor.
- Navigate to View | Update Frequency Very often (1 sec).
- To see the CPU graphs, navigate to Window | CPU History.
For all operating systems
If you are using Windows or macOS or any other OS:
- Rearrange your monitoring tool and your code editor so that they are side by side.
- Wait for the CPUs to settle and then press Enter to start the stopwatch and run the query. The result should be a number of elapsed milliseconds, as shown in the following output:
Calculating Fibonacci sequence up to term 45. Please wait...
17,624 elapsed milliseconds.
Results: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1,597 2,584 4,181 6,765 10,946 17,711 28,657 46,368 75,025 121,393 196,418 317,811 514,229 832,040 1,346,269 2,178,309 3,524,578 5,702,887 9,227,465 14,930,352 24,157,817 39,088,169 63,245,986 102,334,155 165,580,141 267,914,296 433,494,437 701,408,733
The monitoring tool will probably show that one or two CPUs were used the most, alternating over time. Others may execute background tasks at the same time, such as the garbage collector, so the other CPUs or cores won't be completely flat, but the work is certainly not being evenly spread among all the possible CPUs or cores. Also, note that some of the logical processors are maxing out at 100%.
- In
Program.cs, modify the query to make a call to theAsParallelextension method and to sort the resulting sequence, because when processing in parallel the results can become misordered, as shown highlighted in the following code:
int[] fibonacciNumbers = numbers.AsParallel()
.Select(number => Fibonacci(number))
.OrderBy(number => number)
.ToArray();
Good Practice: Never call
AsParallelat the end of a query. This does nothing. You must perform at least one operation after the call toAsParallelfor that operation to be parallelized. .NET 6 introduced a code analyzer that will warn about this type of misuse.
- Run the code, wait for CPU charts in your monitoring tool to settle, and then press Enter to start the stopwatch and run the query. This time, the application should complete in less time (although it might not be as less as you might hope for—managing those multiple threads takes extra effort!):
Calculating Fibonacci sequence up to term 45. Please wait...
9,028 elapsed milliseconds.
Results: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1,597 2,584 4,181 6,765 10,946 17,711 28,657 46,368 75,025 121,393 196,418 317,811 514,229 832,040 1,346,269 2,178,309 3,524,578 5,702,887 9,227,465 14,930,352 24,157,817 39,088,169 63,245,986 102,334,155 165,580,141 267,914,296 433,494,437 701,408,733
- The monitoring tool should show that all CPUs were used equally to execute the LINQ query and note that none of the logical processors max out at 100% because the work is more evenly spread.