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 aCancellationTokento 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.