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
foreachinstead (await foreachorforeachwithTask.WhenAll).
- Small Workload per Iteration If each iteration is fast/lightweight, the overhead of parallelism can slow things down.
- Order Matters:
Parallel.ForEachdoes 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.ForEachruns 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.ForEachmay 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