Parallel Programming with PLINQ & Parallel.For
Lesson 22 โข Advanced Track
What You'll Learn
- Parallelise CPU-bound loops with Parallel.For and Parallel.ForEach
- Control the degree of parallelism with ParallelOptions
- Turn any LINQ query into a parallel query with .AsParallel() (PLINQ)
- Preserve ordering in PLINQ with .AsOrdered()
- Avoid race conditions with Interlocked, lock, and concurrent collections
- Use ConcurrentBag, ConcurrentDictionary, and ConcurrentQueue for thread safety
๐ก Real-World Analogy
Async is like a waiter serving multiple tables โ one person doing many things efficiently by not blocking. Parallel is like hiring more cooks โ multiple people doing different parts of the work simultaneously. Use async for I/O-bound work (waiting for databases, APIs). Use parallel for CPU-bound work (crunching numbers, processing images). They solve different problems.
๐ Async vs Parallel
| Feature | Async/Await | Parallel |
|---|---|---|
| Best for | I/O-bound (network, disk) | CPU-bound (computation) |
| Threads used | 1 (non-blocking) | Multiple (thread pool) |
| Scaling factor | Thousands of concurrent ops | Limited by CPU cores |
| Shared state | Usually not an issue | Race conditions risk |
| API | async/await, Task | Parallel.For, PLINQ |
1. Parallel.For & Parallel.ForEach
Parallel.For distributes loop iterations across thread pool threads. Each iteration runs independently on a different core. Use MaxDegreeOfParallelism to limit how many threads run simultaneously โ useful for resource-constrained environments.
Parallel.For & ForEach
Compare sequential vs parallel loops and control parallelism.
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
class Program
{
// Simulate CPU-intensive work
static double HeavyComputation(int n)
{
double result = 0;
for (int i = 0; i < 100_000; i++)
result += Math.Sqrt(i * n);
return result;
}
static void Main()
{
const int items = 100;
// Sequential
var sw = Stopwatch.StartNew();
double[] seqResults = new double[item
...2. PLINQ โ Parallel LINQ
Add .AsParallel() to any LINQ query to parallelise it. PLINQ partitions the data and processes chunks on different threads. Use .AsOrdered() if you need results in the original order (at a small performance cost).
PLINQ
Find primes with parallel LINQ and control ordering.
using System;
using System.Diagnostics;
using System.Linq;
class Program
{
static bool IsPrime(int n)
{
if (n < 2) return false;
for (int i = 2; i <= Math.Sqrt(n); i++)
if (n % i == 0) return false;
return true;
}
static void Main()
{
int range = 1_000_000;
// Sequential LINQ
var sw = Stopwatch.StartNew();
int seqCount = Enumerable.Range(2, range)
.Where(IsPrime)
.Count();
s
...3. Thread Safety & Concurrent Collections
When parallel code shares state, you get race conditions โ two threads modifying the same variable simultaneously. Use Interlocked for atomic operations, or thread-safe collections like ConcurrentBag and ConcurrentDictionary.
Thread Safety
Fix race conditions with Interlocked and concurrent collections.
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// โ WRONG โ shared counter without synchronization
int unsafeCount = 0;
Parallel.For(0, 100_000, _ =>
{
unsafeCount++; // Race condition!
});
Console.WriteLine($"Unsafe count (should be 100000): {unsafeCount}");
// โ
Option 1: Interlocked (atomic operations)
int safeCou
...Pro Tips
- ๐ก Don't parallelise everything: Parallel overhead (thread management, synchronisation) means small workloads are actually slower in parallel. Profile first.
- ๐ก PLINQ's sweet spot: Data sets with 10,000+ items and CPU-intensive per-item work. For small collections, sequential LINQ is faster.
- ๐ก Prefer Interlocked over lock:
Interlocked.Incrementis lock-free and much faster than wrapping in alockstatement. - ๐ก Use Partitioner for uneven work:
Partitioner.Create(0, items, chunkSize)gives you control over how work is divided.
Common Mistakes
- Using Parallel for I/O:
Parallel.ForEachwith HTTP calls blocks thread pool threads. UseTask.WhenAllwith async instead. - Shared mutable state:
count++in a Parallel.For body is a race condition. UseInterlocked.Incrementor local aggregation. - PLINQ without AsOrdered: By default, PLINQ returns results in arbitrary order. If order matters, add
.AsOrdered(). - Too many threads: Setting
MaxDegreeOfParallelism = 100on an 8-core machine causes excessive context switching. Match to CPU cores. - Exceptions in parallel: Parallel methods wrap exceptions in
AggregateException. Catch and inspect.InnerExceptions.
๐ Lesson Complete
- โ
Parallel.ForandParallel.ForEachparallelise CPU-bound loops - โ
MaxDegreeOfParallelismcontrols how many threads run simultaneously - โ
PLINQ:
.AsParallel()turns any LINQ query into a parallel query - โ
.AsOrdered()preserves element order in PLINQ results - โ
Interlockedprovides lock-free atomic operations for shared counters - โ
ConcurrentBag,ConcurrentDictionaryfor thread-safe collections - โ Next lesson: Memory Management & Garbage Collector Internals
Sign up for free to track which lessons you've completed and get learning reminders.