Skip to content

C# Parallel Programming — Parallel.For, PLINQ, and Concurrent Collections

DodaTech Updated 2026-06-28 6 min read

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

  1. When should you use Parallel.ForEach instead of async/await?
  2. What is the purpose of ConcurrentDictionary vs regular Dictionary?
  3. How do you control the degree of parallelism in PLINQ?
  4. What is the difference between Parallel.For and a regular for loop?
  5. 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

What is the difference between async/await and Parallel.For?

async/await is for non-blocking I/O. Parallel.For is for CPU-bound multi-core work. They solve different problems.

Is PLINQ faster than sequential LINQ?

For CPU-bound operations on large datasets, yes. For small datasets or I/O, PLINQ overhead makes it slower. Measure before optimizing.

What is MaxDegreeOfParallelism?

It limits how many operations run concurrently. Default is ProcessorCount. For CPU-bound work, use ProcessorCount or slightly less.

Can Parallel.ForEach be used with async delegates?

No. Parallel.ForEach uses synchronous delegates. Use Task.WhenAll with async methods for I/O-bound concurrency.

What is the difference between ConcurrentBag and ConcurrentQueue?

ConcurrentQueue is FIFO (first-in, first-out). ConcurrentBag has no ordering and is optimized for the common case where items are added and consumed by the same thread.

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 and Memory for high-performance memory handling.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro