This post explains what C# Parallel.ForEach
Loop is? how it works and when and how to use it effectively to enable data parallelism over any System.Collections.IEnumerable
or System.Collections.Generic.IEnumerable<T>
data source.
Understanding C# Parallel.ForEach
Getting Started
As application complexity and performance demands grow, developers increasingly rely on multi-threading to enhance execution speed, particularly for data processing tasks. In C#, the Parallel.ForEach
loop offers a simple and efficient way to execute iterations concurrently, effectively utilizing multiple processor cores.
What is C# Parallel.ForEach
?
Parallel.ForEach
is part of the Task Parallel Library (TPL) in .NET, and It's a parallelized version of the regular foreach loop and is defined in the System.Threading.Tasks
namespace which allows you to iterate over a collection with multiple threads, automatically managing the distribution of work across available cores.
Parallel.ForEach
was introduced in .NET Framework 4.0, which was released in April 2010 to make multi-threaded and parallel programming more accessible and scalable on multi-core systems.
- Namespace:
System.Threading.Tasks
- Library: Part of the Task Parallel Library (TPL)
- Framework Version: .NET Framework 4.0
- Also Available In:
- .NET Core (from version 1.0 onward)
- .NET 5/6/7/8+ (as part of the unified .NET platform)
Parallel.ForEach Performance
Parallel.ForEach
can improve performance because the loop partitions the source collection and schedules the work on multiple threads based on the system environment means The more processors on the system, the faster the parallel method runs.
- The size of the collection
- The complexity of the operation
- The number of processor cores
- Thread contention or blocking
In C#, Parallel.ForEach
can significantly improve performance by executing iterations of a loop concurrently. However, it's not always the right tool for the job. Here's a guide on when to use and when to avoid Parallel.ForEach
.
When to Use
- CPU-bound Operations
- Use it when you're doing intensive computations in each iteration.
- Example: image processing, mathematical calculations, or parsing large data sets.
- Independent Iterations
- Each iteration should be independent and not depend on results of others.
- No shared state (or well-synchronized shared state).
- Large Datasets
- Effective with a large collection of items (hundreds or thousands).
- For small collections, parallelism overhead might outweigh the benefits.
- Multi-core Machines
- Most beneficial on systems with multiple cores or processors.
- It utilizes all available cores for concurrent execution.
When to Avoid
- I/O-bound Workloads
- Avoid for disk, network, or database I/O (e.g., reading files, calling APIs).
- Use async/await with
foreach
instead (await foreach
orforeach
withTask.WhenAll
).
- Small Workload per Iteration If each iteration is fast/lightweight, the overhead of parallelism can slow things down.
- Order Matters:
Parallel.ForEach
does not guarantee order. Use a regular foreach if order is important. - Shared State Without Synchronization: If your loop modifies shared variables without proper locking, you risk race conditions.
- UI Applications (e.g., WPF, WinForms)
Parallel.ForEach
runs on background threads and can't update UI controls directly.- Use async patterns with proper marshaling to the UI thread.
- Controlling Parallelism is Important If you need throttling, cancellation, or progress reporting,
Parallel.ForEach
may be less flexible thanTask
-based approaches.
Basic Syntax
Parallel.ForEach<TSource>(
IEnumerable<TSource> source,
Action<TSource> body
);
TSource
: The type of elements in the source collection.source
: The collection to iterate over.body
: The delegate that performs the operation on each element.
Example: Basic Usage
Here’s a basic example that processes a list of numbers in parallel.using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
Parallel.ForEach(numbers, number =>
{
Console.WriteLine($"Processing {number} on thread {Task.CurrentId}");
});
Console.WriteLine("Finished processing.");
}
}
Output: might look something like this (order may vary)
Processing 1 on thread 1
Processing 2 on thread 2
Processing 3 on thread 3
...
Finished processing.
Example: Advanced Usage
Using ParallelOptions
You can configure behavior with ParallelOptions
, such as limiting the number of concurrent tasks or handling cancellations.
ParallelOptions options = new ParallelOptions
{
MaxDegreeOfParallelism = 4 // limit to 4 concurrent threads
};
Parallel.ForEach(numbers, options, number =>
{
Console.WriteLine($"Processing {number}");
});
Cancellation Support
CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions options = new ParallelOptions
{
CancellationToken = cts.Token
};
try
{
Parallel.ForEach(numbers, options, number =>
{
if (number == 3)
cts.Cancel(); // simulate cancellation
Console.WriteLine($"Processing {number}");
});
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was canceled.");
}
Thread-Safety
If you're writing to shared variables inside the loop, use thread-safe techniques such aslock
, ConcurrentBag
, Interlocked
, or PLINQ features.
int total = 0;
object lockObj = new object();
Parallel.ForEach(numbers, number =>
{
lock (lockObj)
{
total += number;
}
});
With Non-Generic Collections
The Parallel.ForEach
loop was introduced to work with generic collections, but it can also be used with non-generic collections by using the Enumerable.Cast
extension method to convert the collection to a generic type, as shown in the following example.
Parallel.ForEach(nonGenericCollection.Cast<object>(),
currentElement =>
{
});
Summary
Parallel.ForEach
is a powerful tool in C# for accelerating operations over large collections by distributing work across multiple threads. While it's easy to use, it should be applied thoughtfully, with consideration of thread-safety and performance overhead. In the right scenarios, it can significantly reduce processing time and help you make the most of modern multi-core processors.
Thanks