C# Parallel Programming — Parallel.For, PLINQ, and Concurrent Collections
In this tutorial, you will learn about C# Parallel Programming. We cover key concepts, practical examples, and best practices to help you master this topic.
C# parallel programming enables multi-core CPU utilization through the Parallel class, PLINQ for parallel queries, and concurrent collections for thread-safe data access across threads.
What You'll Learn
You will master parallel programming in C#: Parallel.For and Parallel.ForEach for data parallelism, PLINQ for parallel queries, ConcurrentDictionary and ConcurrentQueue for thread-safe collections, when to parallelize, and common pitfalls in .NET parallel code.
Why It Matters
Modern CPUs have multiple cores. Parallel programming lets you utilize all cores for CPU-bound work, reducing processing time significantly. However, incorrect parallelism introduces race conditions and deadlocks. Understanding when and how to parallelize is essential for high-performance .NET applications.
Real-World Use
Data Pipelines use Parallel.ForEach for batch transformations. Image processing uses Parallel.For for pixel manipulation. Web crawlers use ConcurrentBag for URL discovery. Log analysis uses PLINQ for parallel querying. Risk calculations use parallel Monte Carlo simulations.
Learning Path
graph LR
A["29: Async Await"] --> B["30: Parallel Programming"]
B --> C["31: Span Memory"]
C --> D["32: Index Ranges"]
D --> E["33: Top-Level Statements"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Parallel.For
using System.Diagnostics;
int[] data = Enumerable.Range(0, 100_000_000).ToArray();
var sw = Stopwatch.StartNew();
long sequentialSum = 0;
for (int i = 0; i < data.Length; i++)
sequentialSum += data[i];
sw.Stop();
Console.WriteLine($"Sequential: {sequentialSum} in {sw.ElapsedMilliseconds}ms");
sw.Restart();
long parallelSum = 0;
object lockObj = new();
Parallel.For(0, data.Length,
() => 0L,
(i, _, localSum) => localSum + data[i],
localSum => { lock (lockObj) parallelSum += localSum; });
sw.Stop();
Console.WriteLine($"Parallel: {parallelSum} in {sw.ElapsedMilliseconds}ms");
Parallel.ForEach
var items = Enumerable.Range(0, 1000).ToList();
var results = new ConcurrentBag<int>();
Parallel.ForEach(items, item =>
{
int processed = ProcessItem(item);
results.Add(processed);
});
int ProcessItem(int x)
{
Thread.Sleep(1);
return x * x;
}
Console.WriteLine($"Processed {results.Count} items");
Controlling Parallelism
var options = new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount / 2,
CancellationToken = CancellationToken.None
};
Parallel.For(0, 100, options, i =>
{
Console.WriteLine($"Processing {i} on thread {Thread.CurrentThread.ManagedThreadId}");
});
PLINQ
var numbers = Enumerable.Range(1, 10_000_000);
var sw = Stopwatch.StartNew();
var sequential = numbers
.Where(n => n % 2 == 0)
.Select(n => Math.Sqrt(n))
.Average();
sw.Stop();
Console.WriteLine($"Sequential: {sequential:F4} in {sw.ElapsedMilliseconds}ms");
sw.Restart();
var parallel = numbers
.AsParallel()
.WithDegreeOfParallelism(Environment.ProcessorCount)
.Where(n => n % 2 == 0)
.Select(n => Math.Sqrt(n))
.Average();
sw.Stop();
Console.WriteLine($"Parallel: {parallel:F4} in {sw.ElapsedMilliseconds}ms");
var plinqResult = numbers
.AsParallel()
.WithDegreeOfParallelism(4)
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
.WithMergeOptions(ParallelMergeOptions.NotBuffered)
.Where(n => n % 3 == 0)
.Select(n => n * n)
.ToList();
ConcurrentDictionary
var dict = new ConcurrentDictionary<string, int>();
dict.TryAdd("key1", 1);
dict["key2"] = 2;
dict.AddOrUpdate("counter", 1, (_, existing) => existing + 1);
dict.AddOrUpdate("counter", 1, (_, existing) => existing + 1);
int value = dict.GetOrAdd("key1", _ => 42);
Console.WriteLine($"counter: {dict["counter"]}");
Parallel.For(0, 100, i =>
{
dict.AddOrUpdate("total", i, (_, existing) => existing + i);
});
ConcurrentQueue and ConcurrentBag
var queue = new ConcurrentQueue<int>();
Parallel.For(0, 100, i => queue.Enqueue(i));
int item;
while (queue.TryDequeue(out item))
Console.Write($"{item} ");
Console.WriteLine();
var bag = new ConcurrentBag<string>();
Parallel.ForEach(new[] { "a", "b", "c", "d", "e" }, s => bag.Add(s));
var bagItems = new List<string>();
while (bag.TryTake(out string? s))
bagItems.Add(s);
When to Parallelize
// GOOD: CPU-bound, independent work
Parallel.ForEach(largeArray, item => ExpensiveOperation(item));
// BAD: I/O-bound (use async/await instead)
// Parallel.ForEach(urls, url => httpClient.GetStringAsync(url));
// BAD: Too-small operations (overhead > benefit)
// Parallel.For(0, 10, i => SimpleOp(i));
// BAD: Without synchronization
// int counter = 0;
// Parallel.For(0, 100, i => counter++); // Race condition!
// Use Interlocked for simple operations
int safeCounter = 0;
Parallel.For(0, 100, i => Interlocked.Increment(ref safeCounter));
Common Mistakes
Mistake 1: Parallelizing I/O-Bound Operations
Use async/await for I/O. Parallel.ForEach blocks threads even during I/O waits.
Mistake 2: Shared Mutable State Without Synchronization
Modifying shared collections from parallel operations causes corruption. Use concurrent collections or locks.
Mistake 3: Parallelizing Too-Small Operations
Parallel overhead (delegate invocation, thread scheduling) can exceed the work itself for small operations.
Mistake 4: Ignoring AggregateException
Parallel loops wrap exceptions in AggregateException. Use catch (AggregateException ae) and flatten.
Mistake 5: Assuming PLINQ Always Improves Performance
PLINQ may choose sequential execution if overhead seems too high. Force with WithExecutionMode. Always measure.
Mistake 6: Not Using Thread-Local State in Parallel.For
Without thread-local state, each iteration locks on shared state. Use the localInit and localFinally overloads.
Practice Questions
- When should you use Parallel.ForEach instead of async/await?
- What is the purpose of ConcurrentDictionary vs regular Dictionary?
- How do you control the degree of parallelism in PLINQ?
- What is the difference between Parallel.For and a regular for loop?
- Write a parallel operation that counts primes up to 1 million.
Challenge
Create a parallel file search tool that searches for a pattern in all .cs files in a directory tree. Use ConcurrentBag for results and measure the speedup over sequential search.
FAQ
Mini Project
Create a parallel image filter:
int[] ApplyFilter(int[] pixels, int width, int height)
{
int[] result = new int[pixels.Length];
Parallel.For(0, height, y =>
{
for (int x = 0; x < width; x++)
{
int idx = y * width + x;
int sumR = 0, sumG = 0, sumB = 0, count = 0;
for (int dy = -1; dy <= 1; dy++)
for (int dx = -1; dx <= 1; dx++)
{
int ny = y + dy, nx = x + dx;
if (ny >= 0 && ny < height && nx >= 0 && nx < width)
{
int pixel = pixels[ny * width + nx];
sumR += (pixel >> 16) & 0xFF;
sumG += (pixel >> 8) & 0xFF;
sumB += pixel & 0xFF;
count++;
}
}
int r = sumR / count, g = sumG / count, b = sumB / count;
result[idx] = (255 << 24) | (r << 16) | (g << 8) | b;
}
});
return result;
}
var pixels = new int[1920 * 1080];
var rng = new Random();
for (int i = 0; i < pixels.Length; i++)
pixels[i] = rng.Next();
Console.WriteLine("Applying blur filter...");
var sw = Stopwatch.StartNew();
var filtered = ApplyFilter(pixels, 1920, 1080);
sw.Stop();
Console.WriteLine($"Filtered in {sw.ElapsedMilliseconds}ms");
var rng2 = new Random();
pixels = new int[100 * 100];
for (int i = 0; i < pixels.Length; i++)
pixels[i] = rng2.Next();
sw.Restart();
filtered = ApplyFilter(pixels, 100, 100);
sw.Stop();
Console.WriteLine($"Small image filtered in {sw.ElapsedMilliseconds}ms");
What's Next
You have mastered parallel programming in C#. The next lesson covers Span
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro