How to Use Parallel.For and Parallel.ForEach in C
Parallel processing in C# allows developers to execute iterations of
a loop concurrently across multiple CPU cores to improve application
performance. Part of the Task Parallel Library (TPL) under the
System.Threading.Tasks namespace, Parallel.For
and Parallel.ForEach handle thread scheduling, load
balancing, and synchronization automatically. This article explains how
to implement both constructs, manage execution options, maintain thread
safety, and determine when parallel loops are the right tool for your
workload.
When to Use Parallel Loops
Use Parallel.For and Parallel.ForEach
when:
- The work inside the loop is CPU-bound (e.g., mathematical calculations, image processing, parsing).
- Each iteration is independent of the others.
- The volume of work per iteration is large enough to offset the overhead of thread management.
Do not use them for I/O-bound operations (such as
database queries or network requests); use asynchronous patterns like
Task.WhenAll instead.
Using Parallel.For
Parallel.For executes a loop where the iterations are
defined by an integer range, similar to a standard for
loop.
Basic Syntax
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
int totalItems = 10;
// Iterates from 0 up to (but not including) totalItems
Parallel.For(0, totalItems, i =>
{
Console.WriteLine($"Processing index {i} on Thread {Environment.CurrentManagedThreadId}");
});
}
}In this example, iterations may execute out of order because the runtime distributes the indices across available processor threads.
Using Parallel.ForEach
Parallel.ForEach operates over an
IEnumerable<T>, functioning like a parallel version
of a standard foreach loop.
Basic Syntax
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static void Main()
{
List<string> files = new() { "file1.txt", "file2.txt", "file3.txt", "file4.txt" };
Parallel.ForEach(files, file =>
{
ProcessFile(file);
});
}
static void ProcessFile(string fileName)
{
Console.WriteLine($"Processing {fileName} on Thread {Environment.CurrentManagedThreadId}");
}
}Controlling Concurrency with ParallelOptions
You can control the maximum number of concurrent threads and pass
cancellation tokens using ParallelOptions.
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
using var cts = new CancellationTokenSource();
var options = new ParallelOptions
{
// Limit to a specific number of concurrent threads
MaxDegreeOfParallelism = Environment.ProcessorCount / 2,
CancellationToken = cts.Token
};
try
{
Parallel.For(0, 100, options, (i, state) =>
{
options.CancellationToken.ThrowIfCancellationRequested();
// Perform CPU-intensive work here
});
}
catch (OperationCanceledException)
{
Console.WriteLine("Execution was cancelled.");
}
}
}Breaking or Stopping a Parallel Loop
To exit early from a parallel loop, use the
ParallelLoopState object passed to the loop delegate:
state.Break(): Ensures that all iterations with an index lower than the current index still run, but prevents future iterations beyond the current one.state.Stop(): Ceases execution as soon as possible, regardless of iteration index.
Parallel.For(0, 1000, (i, state) =>
{
if (i == 50)
{
state.Stop();
return;
}
// Work performed here
});Ensuring Thread Safety
Because iterations execute simultaneously, shared data must be protected against race conditions.
1. Atomic Operations
Use the Interlocked class for simple numeric
aggregations.
int totalSum = 0;
Parallel.For(1, 101, i =>
{
Interlocked.Add(ref totalSum, i);
});2. Thread-Safe Collections
Avoid standard collections like List<T> or
Dictionary<TKey, TValue>. Instead, use thread-safe
alternatives from System.Collections.Concurrent, such as
ConcurrentBag<T> or
ConcurrentDictionary<TKey, TValue>.
using System.Collections.Concurrent;
var results = new ConcurrentBag<int>();
Parallel.ForEach(Enumerable.Range(1, 50), number =>
{
results.Add(number * 2);
});Summary Checklist
- Verify Iteration Independence: Ensure no iteration depends on the state or completion order of another.
- Handle Exceptions: Unhandled exceptions in parallel
loops are aggregated and thrown as an
AggregateException. - Avoid Over-Parallelization: For loops with minimal processing per element, the scheduling overhead can make parallel execution slower than sequential execution. Benchmark using tools like BenchmarkDotNet when optimizing critical paths.