Course contentsShow
← C#
Lesson 660 of 1,9086. Asynchronous Programming and ConcurrencyFree lesson

Managing Parallel Loop State

Maintain thread-safe local state within parallel loops using thread-local initialization.

In this lesson, you’ll learn how to maintain efficient, thread-safe local counters and buffers inside a parallel loop using thread-local initialization.

The Challenge of Shared State

When using Parallel.For or Parallel.ForEach, your code runs on multiple threads simultaneously. If you try to update a shared variable (like a counter) directly inside the loop, you encounter a "race condition." You could use the lock statement to make it thread-safe, but that forces your threads to wait in line, destroying the performance gains of parallelism.

The Solution: Thread-Local State

Instead of sharing one variable, you can give each thread its own "private" copy of a variable. The parallel loop initializes this state once for every thread involved, lets the thread work on its private copy without any locking, and finally merges the results at the end.

We achieve this using the localInit and localFinally parameters in Parallel.ForEach:

int[] numbers = Enumerable.Range(1, 1000).ToArray();
long totalSum = 0;

Parallel.ForEach(
    numbers,
    // 1. Initialize a local state (this runs once per thread)
    () => 0, 
    // 2. Perform work using the local thread variable
    (num, loopState, localSum) => localSum + num,
    // 3. Merge the thread-local result back to the global sum
    (localSum) => Interlocked.Add(ref totalSum, localSum)
);

How it Works

  1. localInit: The delegate () => 0 runs once for every thread. If 4 threads are processing your loop, 4 separate localSum variables are created.
  2. The Loop Body: The second delegate receives the localSum. Because this is local to the thread, no lock is required; it is naturally thread-safe and incredibly fast.
  3. localFinally: Once a thread finishes its portion of the work, the last delegate executes. We use Interlocked.Add here to safely combine the thread's final private sum into the main totalSum. Because this only happens once per thread, the performance overhead is negligible compared to locking on every single iteration.

Key Takeaway: Use thread-local initialization to provide each parallel worker with its own private state, eliminating the need for slow, constant locking.