How to use Parallel.For and Parallel.ForEach in C#

By FoxLearn 1/6/2025 4:28:48 AM   244
Parallel programming in .NET enables more efficient utilization of system resources and provides greater control over program execution.

.NET introduced support for parallel programming in .NET Framework 4.

Concurrency and parallelism in .NET Core

Concurrency and parallelism are key concepts in .NET and .NET Core, with subtle differences between them. In concurrent execution, tasks (T1 and T2) take turns running, with one task waiting while the other executes. In parallel execution, both tasks run simultaneously. Achieving parallelism requires a multi-core CPU.

Parallel.For and Parallel.ForEach in .NET Core

The Parallel.For loop allows iterations to run in parallel across multiple threads, similar to a regular for loop but with parallel execution. The Parallel.ForEach method divides the work into separate tasks for each item in a collection. Unlike the sequential foreach loop, Parallel.ForEach runs tasks in parallel across multiple threads.

Parallel.ForEach vs foreach in C#

Consider the following example, where we check whether a number is a palindrome by using a method that accepts a string as a parameter and returns true if it is a palindrome.

static bool IsPalindrome(string str)
{
    int start = 0;
    int end = str.Length - 1;
    while (start < end)
    {
        if (str[start] != str[end])
            return false;
        start++;
        end--;
    }
    return true;
}

We will now use ConcurrentDictionary to store the palindromes and the thread IDs that processed them. Since each palindrome string is unique, we can store the string as a key and the managed thread ID as the value. The ConcurrentDictionary class in .NET, found in the System.Collections.Concurrent namespace, provides thread-safe and lock-free implementations for dictionary-like collections.

The following two methods both use the IsPalindrome method to check if a string is a palindrome, store the palindromes and managed thread IDs in a ConcurrentDictionary, and return the dictionary. The first method uses concurrency, and the second method uses parallelism.

private static ConcurrentDictionary<string, int> GetPalindromesConcurrent(IList<string> words)
{
    var palindromes = new ConcurrentDictionary<string, int>();
    foreach (var word in words)
    {
        if (IsPalindrome(word))
        {
            palindromes.TryAdd(word, Thread.CurrentThread.ManagedThreadId);
        }
    }
    return palindromes;
}

private static ConcurrentDictionary<string, int> GetPalindromesParallel(IList<string> words)
{
    var palindromes = new ConcurrentDictionary<string, int>();
    Parallel.ForEach(words, word =>
    {
        if (IsPalindrome(word))
        {
            palindromes.TryAdd(word, Thread.CurrentThread.ManagedThreadId);
        }
    });
    return palindromes;
}

In this example, the GetPalindromesConcurrent method iterates over the list of words sequentially, checking each one for being a palindrome and storing the result in the ConcurrentDictionary. In contrast, the GetPalindromesParallel method processes the words concurrently, using Parallel.ForEach, which allows multiple threads to work simultaneously.

Concurrent vs parallel in C#

The following code snippet shows how you can invoke the GetEvenNumbersConcurrent method to retrieve all even numbers between 1 and 100, along with the managed thread IDs.

static void Main(string[] args)
{
    var numbers = Enumerable.Range(1, 100).ToList();
    var result = GetEvenNumbersConcurrent(numbers);
    foreach (var number in result)
    {
        Console.WriteLine($"Even Number: {string.Format("{0:0000}", number.Key)}, Managed Thread Id: {number.Value}");
    }
    Console.Read();
}

private static ConcurrentDictionary<int, int> GetEvenNumbersConcurrent(IList<int> numbers)
{
    var evens = new ConcurrentDictionary<int, int>();
    foreach (var number in numbers)
    {
        if (number % 2 == 0)
        {
            evens.TryAdd(number, Thread.CurrentThread.ManagedThreadId);
        }
    }
    return evens;
}

When executed, the output will show that the managed thread ID remains the same for each even number, as concurrency was used (one thread processes the numbers).

For example, here's how you can retrieve even numbers between 1 and 100 using parallelism:

static void Main(string[] args)
{
    var numbers = Enumerable.Range(1, 100).ToList();
    var result = GetEvenNumbersParallel(numbers);
    foreach (var number in result)
    {
        Console.WriteLine($"Even Number: {string.Format("{0:0000}", number.Key)}, Managed Thread Id: {number.Value}");
    }
    Console.Read();
}

private static ConcurrentDictionary<int, int> GetEvenNumbersParallel(IList<int> numbers)
{
    var evens = new ConcurrentDictionary<int, int>();
    Parallel.ForEach(numbers, number =>
    {
        if (number % 2 == 0)
        {
            evens.TryAdd(number, Thread.CurrentThread.ManagedThreadId);
        }
    });
    return evens;
}

When executed, the output will show that different thread IDs are used for different even numbers, because the program uses Parallel.ForEach, allowing multiple threads to process the numbers concurrently.

Limit the degree of parallelism in C#

The degree of parallelism is a key concept that defines the maximum number of threads that can run concurrently when processing tasks.

By default, methods like Parallel.For or Parallel.ForEach don’t limit the number of tasks they spawn. However, we can control this behavior by setting the MaxDegreeOfParallelism property.

Let’s look at an example where we want to limit the degree of parallelism to 50% of the system’s available processors.

private static ConcurrentDictionary<int, int> GetEvenNumbersWithLimitedParallelism(IList<int> numbers)
{
    var evens = new ConcurrentDictionary<int, int>();
    
    // Define ParallelOptions to limit the degree of parallelism
    var parallelOptions = new ParallelOptions
    {
        MaxDegreeOfParallelism = Convert.ToInt32(Math.Ceiling((Environment.ProcessorCount * 0.5) * 2.0)) // 50% of available CPU resources
    };
    
    Parallel.ForEach(numbers, parallelOptions, number =>
    {
        if (number % 2 == 0)
        {
            evens.TryAdd(number, Thread.CurrentThread.ManagedThreadId);
        }
    });
    
    return evens;
}

In this example:

  • The ParallelOptions object is used to configure the parallel execution of the Parallel.ForEach loop.
  • We set MaxDegreeOfParallelism to 50% of the system’s processor resources by calculating the number of processors (via Environment.ProcessorCount), multiplying it by 0.5, and assuming each processor has two cores (hence multiplying by 2).
  • This limits the number of threads that can run in parallel, preventing the system from becoming overburdened.

Determine if a parallel loop is complete in C#

In C#, both Parallel.For and Parallel.ForEach methods return a ParallelLoopResult object, which can be used to check whether the parallel loop has completed execution.

Let’s go through an example where we use Parallel.ForEach to process a collection of numbers, checking if each number is divisible by 3. Once the loop is complete, we can check the IsCompleted property of the ParallelLoopResult to determine if the parallel execution has finished.

private static ConcurrentDictionary<int, int> GetDivisibleByThreeNumbers(IList<int> numbers)
{
    var divisibleByThree = new ConcurrentDictionary<int, int>();

    // Initialize ParallelOptions to set the maximum degree of parallelism if needed
    var parallelOptions = new ParallelOptions
    {
        MaxDegreeOfParallelism = Convert.ToInt32(Math.Ceiling(Environment.ProcessorCount * 0.5)) // Limit to 50% of CPU resources
    };

    // Execute Parallel.ForEach to process the numbers in parallel
    ParallelLoopResult parallelLoopResult = Parallel.ForEach(numbers, parallelOptions, number =>
    {
        if (number % 3 == 0)
        {
            divisibleByThree.TryAdd(number, Thread.CurrentThread.ManagedThreadId);
        }
    });

    // Check if the parallel loop has completed
    Console.WriteLine("IsParallelLoopCompleted: {0}", parallelLoopResult.IsCompleted);

    return divisibleByThree;
}

In this example:

  • We use the Parallel.ForEach method to iterate over the numbers list, checking if each number is divisible by 3.
  • The ParallelLoopResult object (parallelLoopResult) contains an IsCompleted property, which will be true if the parallel loop executed successfully without interruption.
  • After the loop completes, we print the status of IsCompleted to indicate whether all iterations finished without being cancelled or encountering errors.