AsyncBarrier
A pooled, allocation-free async barrier that synchronizes multiple participants using ValueTask-based waiters.
Overview
AsyncBarrier is an async-compatible barrier synchronization primitive. It allows a fixed number of participants to synchronize at a barrier point, releasing all of them when the last participant arrives. The barrier automatically resets for the next phase.
This implementation follows the same semantics as the .NET System.Threading.Barrier class but provides async support with ValueTask-based waiters.
Usage
Basic Usage
using CryptoHives.Foundation.Threading.Async.Pooled;
private readonly AsyncBarrier _barrier = new AsyncBarrier(3);
public async Task ParticipantWorkAsync(int participantId, CancellationToken ct)
{
// Phase 1
await DoPhase1WorkAsync(participantId);
Console.WriteLine($"Participant {participantId} completed phase 1");
// Wait for all participants
await _barrier.SignalAndWaitAsync(ct);
// Phase 2 - all participants continue together
await DoPhase2WorkAsync(participantId);
Console.WriteLine($"Participant {participantId} completed phase 2");
}
With Post-Phase Action
// Create barrier with post-phase action
private readonly AsyncBarrier _barrier = new AsyncBarrier(3, b =>
{
Console.WriteLine($"Phase {b.CurrentPhase} completed by all participants");
});
public async Task ParticipantWorkAsync(int participantId, CancellationToken ct)
{
await DoPhase1WorkAsync(participantId);
await _barrier.SignalAndWaitAsync(ct);
// Post-phase action runs before participants are released
await DoPhase2WorkAsync(participantId);
}
Handling Post-Phase Exceptions
var barrier = new AsyncBarrier(3, b =>
{
if (b.CurrentPhase == 2)
{
throw new InvalidOperationException("Phase 2 failed");
}
});
try
{
await barrier.SignalAndWaitAsync(ct);
}
catch (BarrierPostPhaseException ex)
{
// All participants receive the same exception
Console.WriteLine($"Post-phase action failed: {ex.InnerException?.Message}");
}
Dynamic Participant Management
// Start with 3 participants
var barrier = new AsyncBarrier(3);
// Add 2 more participants
barrier.AddParticipants(2);
Console.WriteLine($"Participants: {barrier.ParticipantCount}"); // 5
// Remove 1 participant
barrier.RemoveParticipant();
Console.WriteLine($"Participants: {barrier.ParticipantCount}"); // 4
Constructors
AsyncBarrier(int, bool, IGetPooledManualResetValueTaskSource?)
public AsyncBarrier(
int participantCount,
bool runContinuationAsynchronously = true,
IGetPooledManualResetValueTaskSource<bool>? pool = null)
Creates a barrier without a post-phase action.
AsyncBarrier(int, Action?, bool, IGetPooledManualResetValueTaskSource?)
public AsyncBarrier(
int participantCount,
Action<AsyncBarrier>? postPhaseAction,
bool runContinuationAsynchronously = true,
IGetPooledManualResetValueTaskSource<bool>? pool = null)
Creates a barrier with an optional post-phase action. The post-phase action is invoked inside the barrier lock after all participants have signaled but before they are released. Hence the post phase action shall be brief and non-blocking to avoid delaying other participants and dead locks.
| Parameter | Description |
|---|---|
participantCount |
The number of participants. Must be greater than zero. |
postPhaseAction |
An action to execute after each phase when all participants have arrived. If this action throws, all participants receive a BarrierPostPhaseException. |
runContinuationAsynchronously |
If true (default), continuations run on the thread pool. |
pool |
Optional custom pool for ValueTaskSource instances. |
Properties
| Property | Type | Description |
|---|---|---|
ParticipantCount |
int |
Gets the total number of participants in the barrier. Changes when participants are added or removed. |
ParticipantsRemaining |
int |
Gets the number of participants that haven't yet signaled in the current phase. |
CurrentPhase |
long |
Gets the current phase number. Increments each time the barrier is released. |
RunContinuationAsynchronously |
bool |
Gets or sets whether continuations run asynchronously. |
Methods
SignalAndWaitAsync
public ValueTask SignalAndWaitAsync(CancellationToken cancellationToken = default)
Signals the barrier and waits for all participants to arrive.
Throws:
InvalidOperationException- If more participants signal than the total number of registered participants.BarrierPostPhaseException- If the post-phase action throws an exception.
AddParticipant / AddParticipants
public long AddParticipant()
public long AddParticipants(int participantCount)
Dynamically adds participants to the barrier. Returns the phase number when the participant(s) are added.
Throws:
ArgumentOutOfRangeException- IfparticipantCountis less than 1.InvalidOperationException- If adding participants would cause an overflow.
RemoveParticipant / RemoveParticipants
public void RemoveParticipant()
public void RemoveParticipants(int participantCount)
Dynamically removes participants from the barrier. If the remaining participants reaches zero (and there are still registered participants), all waiters are released and the barrier advances to the next phase.
Throws:
ArgumentOutOfRangeException- IfparticipantCountis less than 1.InvalidOperationException- If there are not enough participants to remove.BarrierPostPhaseException- If the post-phase action throws an exception during phase advancement.
Post-Phase Action
The post-phase action is an optional callback that is executed after all participants have signaled but before they are released. This is useful for:
- Logging phase completion
- Validating intermediate results
- Preparing for the next phase
Important: The CurrentPhase property still reflects the phase number before increment when the post-phase action is called.
If the post-phase action throws an exception:
- The exception is wrapped in a
BarrierPostPhaseException - All waiting participants receive this exception
- The phase still advances
- The barrier continues to function normally for subsequent phases
Performance
- O(1) signal operation for non-final participant
- O(n) broadcast when last participant signals
- Zero allocations when last participant signals with no waiters
- Automatic phase reset after each release
Benchmark Results
The following benchmarks compares the pooled AsyncBarrier against the .NET System.Threading.Barrier and a reference implementations from Stephen Toub's blog.
Signal and Wait Benchmark
Measures the performance of the SignalAndWaitAsync operation across multiple participants. The .NET implementation uses blocking waits and is out of contest because multiple threads are needed to contest the barrier. Similarly the RefImpl is out of contest because it does not support cancellation tokens. So the run is mainly a proof that there are no memory allocations for the pooled version in contested waits.
| Description | ParticipantCount | Mean | Ratio | Allocated |
|---|---|---|---|---|
| SignalAndWait · AsyncBarrier · Pooled | 1 | 12.09 ns | 1.00 | - |
| SignalAndWait · Barrier · Standard | 1 | 451.79 ns | 37.36 | 238 B |
| SignalAndWait · AsyncBarrier · RefImpl | 1 | 900.88 ns | 74.50 | 8334 B |
| SignalAndWait · AsyncBarrier · Pooled | 10 | 279.76 ns | 1.00 | - |
| SignalAndWait · AsyncBarrier · RefImpl | 10 | 1,659.14 ns | 5.93 | 10545 B |
| SignalAndWait · Barrier · Standard | 10 | 4,062.92 ns | 14.53 | 1392 B |
Benchmark Analysis
Key Findings:
Per-Phase Efficiency: The barrier automatically resets after each phase, allowing efficient multi-phase synchronization without manual reset or reallocation of the barrier.
Participant Scaling: Performance scales linearly with participant count due to the O(n) broadcast when the last participant signals.
Memory Efficiency: Pooled
IValueTaskSourceinstances reduce allocation pressure for waiters, especially beneficial in scenarios with many barrier phases.Cancellation Handling: Proper participant count restoration on cancellation ensures the barrier remains consistent, though cancellation in barrier scenarios should be used carefully.
When to Choose AsyncBarrier:
- Multi-phase parallel algorithms
- Iterative computations where workers must synchronize between phases
- Pipeline processing with synchronized stages
Design Considerations:
- Unlike
System.Threading.Barrier, this implementation is async-native - The pooled approach benefits scenarios with many phases
- Dynamic participant changes (
AddParticipant/RemoveParticipant) allow flexible coordination patterns
Cancellation Behavior
When a waiting participant is cancelled:
- The participant's
ParticipantsRemainingcount is restored - Other participants continue waiting
- The barrier remains in a consistent state
Comparison with System.Threading.Barrier
| Feature | AsyncBarrier | System.Threading.Barrier |
|---|---|---|
| Async support | Native ValueTask |
None (blocking only) |
| Post-phase action | Action<AsyncBarrier> |
Action<Barrier> |
| BarrierPostPhaseException | Yes | Yes |
| Allocation overhead | Minimal (pooled) | None (sync) |
| Cancellation | Full support | Full support |
| Dynamic participants | Yes | Yes |
See Also
- Threading Package Overview
- AsyncAutoResetEvent - Auto-reset event variant
- AsyncManualResetEvent - Manual-reset event variant
- AsyncReaderWriterLock - Async reader-writer lock
- AsyncLock - Async mutual exclusion lock
- AsyncCountdownEvent - Async countdown event
- AsyncSemaphore - Async semaphore primitive
- Benchmarks - Benchmark description
© 2026 The Keepers of the CryptoHives