What is Parallelism?
Parallelism means doing more than one task at exactly the same instant, using more than one worker. In a computer, the workers are CPU cores.
In the previous lesson one chef switched between three dishes (concurrency). Now imagine three chefs, each cooking one dish at the same time. That is parallelism: real work happening at the same moment on different hands.
Why parallelism needs multiple cores
A CPU core can execute only one instruction stream at a time. Most modern laptops have 4, 8 or more cores, so a program can run several pieces of work truly side by side. You can check how many cores your machine has:
Console.WriteLine($"This machine has {Environment.ProcessorCount} logical cores");
On a machine with only one core, there can be concurrency (switching), but there cannot be real parallelism.
What kind of work benefits from parallelism?
Parallelism helps with CPU-bound work: work where the CPU is busy calculating the whole time, for example:
- Processing thousands of images (resize, compress)
- Big calculations: reports, statistics, encryption, prime numbers
- Parsing and transforming large files in memory
It does not help much with I/O work like database calls or web requests, because there the CPU is mostly waiting, not calculating. For waiting work we use async/await.
Parallel.For and Parallel.ForEach
The easiest way to use parallelism in C# is the Parallel class in System.Threading.Tasks. It splits a loop across the available cores for you.
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
string[] files = { "a.jpg", "b.jpg", "c.jpg", "d.jpg" };
Parallel.ForEach(files, file =>
{
Console.WriteLine($"Processing {file} on thread {Environment.CurrentManagedThreadId}");
});
}
}
Different thread numbers prove that the items were processed on different threads at the same time. The order is not fixed, because the threads race each other.
Measuring the speed-up
Let's count prime numbers, a pure CPU job, first with a normal loop and then with Parallel.For.
using System;
using System.Diagnostics;
using System.Threading.Tasks;
class Program
{
static void Main()
{
int[] limits = { 2_000_000, 2_000_000, 2_000_000, 2_000_000 };
Stopwatch watch = Stopwatch.StartNew();
for (int i = 0; i < limits.Length; i++)
{
CountPrimes(limits[i]);
}
Console.WriteLine($"Normal for loop : {watch.ElapsedMilliseconds} ms");
watch.Restart();
Parallel.For(0, limits.Length, i =>
{
CountPrimes(limits[i]);
});
Console.WriteLine($"Parallel.For loop: {watch.ElapsedMilliseconds} ms");
}
static int CountPrimes(int max)
{
int count = 0;
for (int n = 2; n <= max; n++)
{
bool isPrime = true;
for (int d = 2; d * d <= n; d++)
{
if (n % d == 0) { isPrime = false; break; }
}
if (isPrime) count++;
}
return count;
}
}
Four equal jobs ran on four cores at the same time, so the parallel loop was roughly 3.5 times faster. It is rarely exactly 4 times, because splitting the work and combining the results also takes a little time.
Other ways to run code in parallel
// 1. Task.Run: run a piece of CPU work on a thread-pool thread
Task<int> t1 = Task.Run(() => CountPrimes(2_000_000));
Task<int> t2 = Task.Run(() => CountPrimes(3_000_000));
int[] counts = await Task.WhenAll(t1, t2); // both counted at the same time
// 2. PLINQ: add AsParallel() to a LINQ query
var bigNumbers = Enumerable.Range(1, 10_000_000)
.AsParallel()
.Where(n => n % 7 == 0)
.ToList();
Things to be careful about
- Small work gets slower, not faster. If each item takes microseconds, the cost of splitting work across threads is bigger than the gain.
- Shared data needs protection. Many threads writing to the same
List<T>or variable at once will corrupt it. UseConcurrentBag<T>,ConcurrentDictionary,lockorInterlocked. - Order is not guaranteed. Don't rely on items being processed in sequence.
- Don't use it for I/O. Calling a web API inside
Parallel.ForEachblocks many threads while they wait. UseTask.WhenAllorParallel.ForEachAsync(.NET 6+) instead.
Summary
- Parallelism means doing several tasks at exactly the same instant on different CPU cores.
- It needs more than one core and is meant for CPU-bound work.
- Use
Parallel.For,Parallel.ForEach,Task.Runor PLINQ (AsParallel). - The speed-up depends on the number of cores and the size of the work.
- Protect shared data and don't expect results in a fixed order.