Barrier Synchronization and Phased Concurrency in C

Barrier synchronization is a coordination pattern that enables multiple concurrent threads to work cooperatively in discrete, sequential steps known as phases. In this article, you will learn the core mechanics of barrier synchronization, how the .NET Barrier class manages thread rendezvous points, and how to implement phased multi-threaded algorithms in C# through practical examples.

Understanding Barrier Synchronization

In concurrent programming, a barrier acts as a stopping point for a predefined group of threads, known as participants. When individual threads reach the barrier, they pause execution until every other participant arrives at the same point. Once the final participant signals its arrival, the barrier releases all threads simultaneously, allowing them to proceed together to the next operational phase.

This synchronization primitive is ideal for problems that can be decomposed into iterative parallel steps, where the input of step N+1 depends on the completed output of step N from all participating threads.

The Barrier Class in .NET

The .NET framework provides barrier synchronization through the System.Threading.Barrier class. Key elements of this class include:

  • Participant Count: The total number of threads that must arrive before the barrier resets and releases them.
  • SignalAndWait(): The primary method called by each thread upon finishing its work for the current phase. It signals the caller's completion and blocks until all participants have called it.
  • Post-Phase Action: An optional delegate (Action<Barrier>) executed after all threads reach the barrier, but before any thread is unblocked to enter the next phase. This delegate executes synchronously on the last thread to arrive, making it safe for consolidating intermediate results or preparing state for the next phase.
  • CurrentPhaseNumber: An integer tracking which execution phase the barrier is currently on.

Implementing Phased Operations in C

The following example demonstrates how three worker threads perform a phased computation using a Barrier. The post-phase action aggregates intermediate data before the next phase begins.

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        const int participants = 3;
        int[] partialResults = new int[participants];

        // Define a Barrier with 3 participants and a post-phase consolidation action
        using Barrier barrier = new Barrier(participants, b =>
        {
            Console.WriteLine($"\n[Phase {b.CurrentPhaseNumber} Complete]");
            int total = 0;
            foreach (var val in partialResults)
            {
                total += val;
            }
            Console.WriteLine($"[Consolidated Phase Total: {total}]\n");
        });

        Task[] tasks = new Task[participants];

        for (int i = 0; i < participants; i++)
        {
            int threadId = i;
            tasks[i] = Task.Run(() =>
            {
                // Phase 0: Initial computation
                partialResults[threadId] = (threadId + 1) * 10;
                Console.WriteLine($"Task {threadId} finished Phase 0 with value {partialResults[threadId]}");
                barrier.SignalAndWait();

                // Phase 1: Computation based on phase 0 completion
                partialResults[threadId] *= 2;
                Console.WriteLine($"Task {threadId} finished Phase 1 with value {partialResults[threadId]}");
                barrier.SignalAndWait();

                // Phase 2: Final cleanup or processing
                partialResults[threadId] += 5;
                Console.WriteLine($"Task {threadId} finished Phase 2 with value {partialResults[threadId]}");
                barrier.SignalAndWait();
            });
        }

        Task.WaitAll(tasks);
        Console.WriteLine("All phased operations completed successfully.");
    }
}

Dynamic Participants and Timeouts

The Barrier class supports dynamic adaptation during runtime:

  • AddParticipant() / AddParticipants(int count): Increases the expected number of participants, useful when work needs to be dynamically scaled out.
  • RemoveParticipant() / RemoveParticipants(int count): Decreases the expected count, preventing deadlocks if a task exits early or fails.
  • Overloaded SignalAndWait: Accepts a timeout or a CancellationToken to handle scenarios where a thread takes too long or the operation is canceled, preventing permanent deadlocks.
// Example using a cancellation token and timeout
try
{
    barrier.SignalAndWait(TimeSpan.FromSeconds(5), cancellationToken);
}
catch (BarrierPostPhaseException ex)
{
    // Thrown if the post-phase action throws an exception
    Console.WriteLine($"Error during post-phase: {ex.InnerException?.Message}");
}
catch (OperationCanceledException)
{
    // Handle cancellation
}

When to Use Barrier Over Other Primitives

While CountdownEvent allows threads to wait for a counter to decrement to zero, it is a one-shot mechanism and must be manually reset. In contrast, Barrier automatically resets its internal counter each time the required participants arrive, advancing to the next phase seamlessly. When concurrent tasks must repeatedly synchronize across sequential processing boundaries, Barrier minimizes manual lock management, reduces thread contention, and simplifies code structure.