Parallel.ForEach
thread safety
multithreading
C#
concurrent programming

Is this use of Parallel.ForEach thread safe?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Parallel.ForEach in .NET is safe as a framework primitive, but your loop body can still be unsafe if it touches shared mutable state. Most bugs come from race conditions in counters, lists, and mutable objects captured by closures. This guide explains how to evaluate safety and how to write Parallel.ForEach code that remains correct under concurrency.

Core Topic Sections

What is thread-safe and what is not

Parallel.ForEach itself handles partitioning and scheduling correctly. Safety problems usually come from what happens inside each iteration.

Safe by default:

  1. Pure calculations on local variables.
  2. Independent writes to unique array indexes.

Unsafe by default:

  1. Incrementing shared counters without atomic operations.
  2. Writing to a shared list collection without synchronization.
  3. Mutating shared non-thread-safe objects.

Classic unsafe pattern

csharp
1using System;
2using System.Collections.Generic;
3using System.Threading.Tasks;
4
5var items = Enumerable.Range(1, 100000).ToList();
6int total = 0;
7
8Parallel.ForEach(items, item =>
9{
10    total += item; // race condition
11});
12
13Console.WriteLine(total);

The result may vary run to run because multiple threads read and write total concurrently.

Fix shared counters with Interlocked

csharp
1using System;
2using System.Linq;
3using System.Threading;
4using System.Threading.Tasks;
5
6var items = Enumerable.Range(1, 100000);
7long total = 0;
8
9Parallel.ForEach(items, item =>
10{
11    Interlocked.Add(ref total, item);
12});
13
14Console.WriteLine(total);

Interlocked operations are atomic and ideal for simple numeric accumulators.

Prefer thread-local accumulation for performance

Atomic operations are safe, but heavy contention can reduce throughput. Use local accumulators and combine at the end.

csharp
1using System;
2using System.Linq;
3using System.Threading;
4using System.Threading.Tasks;
5
6var items = Enumerable.Range(1, 1_000_000);
7long total = 0;
8
9Parallel.ForEach(
10    items,
11    () => 0L,
12    (item, state, localTotal) =>
13    {
14        localTotal += item;
15        return localTotal;
16    },
17    localTotal => Interlocked.Add(ref total, localTotal)
18);
19
20Console.WriteLine(total);

This often scales better for aggregation-heavy workloads.

Use concurrent collections for shared output

If each iteration produces output items, do not push into a normal shared list directly. Use concurrent collections or merge per-thread buffers.

csharp
1using System.Collections.Concurrent;
2using System.Threading.Tasks;
3
4var input = new[] { "a", "b", "c", "d" };
5var bag = new ConcurrentBag<string>();
6
7Parallel.ForEach(input, value =>
8{
9    bag.Add(value.ToUpperInvariant());
10});
11
12Console.WriteLine(bag.Count);

ConcurrentBag is good for unordered accumulation.

Determinism and ordering concerns

Parallel.ForEach does not guarantee processing order or output order. If order matters, either:

  1. Store outputs with original index and sort afterward.
  2. Use sequential processing for order-sensitive workflows.

Correctness is more important than raw parallel speed for deterministic pipelines.

Exception behavior

Exceptions from parallel loop bodies are aggregated and rethrown as AggregateException.

Pattern:

csharp
1try
2{
3    Parallel.ForEach(data, item => Process(item));
4}
5catch (AggregateException ex)
6{
7    foreach (var inner in ex.InnerExceptions)
8    {
9        Console.WriteLine(inner.Message);
10    }
11}

Do not swallow exceptions silently, otherwise partial work can hide failures.

Avoid over-parallelization

Not every loop benefits from parallel execution. It can hurt performance when:

  1. Per-item work is tiny.
  2. Loop body is I/O bound with blocking calls.
  3. Shared contention dominates computation.

Measure before and after using realistic datasets and production-like environments.

Practical thread-safety checklist

Before shipping Parallel.ForEach code:

  1. Identify every captured variable in the loop body.
  2. Verify each shared write is atomic or synchronized.
  3. Replace non-thread-safe collections with safe alternatives.
  4. Add concurrency stress tests for flaky race detection.

This checklist catches most failures early.

Common Pitfalls

  • Assuming Parallel.ForEach makes unsafe loop bodies safe automatically.
  • Updating shared counters with plain increment operations.
  • Writing to a normal shared list from multiple threads without protection.
  • Expecting deterministic iteration order from parallel execution.
  • Parallelizing very small workloads and regressing performance.

Summary

  • 'Parallel.ForEach is safe infrastructure, but loop body safety is your responsibility.'
  • Use Interlocked, thread-local reduction, and concurrent collections.
  • Treat ordering as non-deterministic unless explicitly reconstructed.
  • Handle AggregateException correctly for reliable diagnostics.
  • Benchmark and stress test to confirm both correctness and performance.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.