AsyncCountdownEvent
A pooled, allocation-free async countdown event that signals when a count reaches zero using ValueTask-based waiters.
Overview
AsyncCountdownEvent is an async-compatible countdown synchronization primitive. It allows one or more tasks to wait until a specified number of signals have been received. It uses pooled IValueTaskSource instances to minimize allocations.
Usage
Basic Usage
using CryptoHives.Foundation.Threading.Async.Pooled;
private readonly AsyncCountdownEvent _countdown = new AsyncCountdownEvent(3);
// Coordinator waits for all workers
public async Task WaitForWorkersAsync(CancellationToken ct)
{
await _countdown.WaitAsync(ct);
Console.WriteLine("All workers completed!");
}
// Each worker signals when done
public void WorkerCompleted()
{
_countdown.Signal();
}
Fan-Out/Fan-In Pattern
public async Task ProcessBatchAsync(IEnumerable<Item> items, CancellationToken ct)
{
var itemList = items.ToList();
var countdown = new AsyncCountdownEvent(itemList.Count);
foreach (var item in itemList)
{
_ = Task.Run(async () =>
{
await ProcessItemAsync(item, ct);
countdown.Signal();
}, ct);
}
await countdown.WaitAsync(ct);
Console.WriteLine("All items processed!");
}
Signal and Wait
public async Task ParticipantWorkAsync(CancellationToken ct)
{
await DoWorkAsync();
await _countdown.SignalAndWaitAsync(ct);
// All participants have completed
}
Constructor
public AsyncCountdownEvent(
int initialCount,
bool runContinuationAsynchronously = true,
IGetPooledManualResetValueTaskSource<bool>? pool = null)
| Parameter | Description |
|---|---|
initialCount |
The initial count. Must be greater than zero. |
runContinuationAsynchronously |
If true (default), continuations are forced to run on the thread pool. |
pool |
Optional custom pool for ValueTaskSource instances. |
Properties
| Property | Type | Description |
|---|---|---|
CurrentCount |
int |
Gets the current count remaining. |
InitialCount |
int |
Gets the initial count. |
IsSet |
bool |
Gets whether the count has reached zero. |
RunContinuationAsynchronously |
bool |
Gets or sets whether continuations run asynchronously. |
Methods
WaitAsync
public ValueTask WaitAsync(CancellationToken cancellationToken = default)
Asynchronously waits for the countdown to reach zero.
Signal
public void Signal()
public void Signal(int signalCount)
Decrements the countdown by one or more.
SignalAndWaitAsync
public ValueTask SignalAndWaitAsync(CancellationToken cancellationToken = default)
Signals the countdown and waits for it to reach zero.
AddCount
public void AddCount()
public void AddCount(int signalCount)
Increments the countdown.
TryAddCount
public bool TryAddCount(int signalCount = 1)
Attempts to increment the countdown. Returns false if already set.
Reset
public void Reset(int count = 0)
Resets the countdown to the specified count, or to the initial count if not specified.
Performance
- O(1) signal and wait operations
- O(n) broadcast to all waiters when count reaches zero
- Zero allocations when count reaches zero with no waiters
- Pooled ValueTaskSource instances for waiters
Benchmark Results
The following benchmarks compare AsyncCountdownEvent against Nito.AsyncEx.AsyncCountdownEvent and reference implementations.
Signal Operation Benchmark
Measures the performance of signaling and waiting on the countdown event.
The standard implementation does not support Task-based waiters, while AsyncCountdownEvent uses pooled IValueTaskSource instances. Hence the tests run only uncontested for standard implementations.
Only the pooled AsyncCountdownEvent is benchmarked in a contested and a uncontested scenario to proof that no memory allocations occur.
The Nito.Async implementation can not be benchmarked due to its internal design which doesn't allow to Reset the event, a new allocation for the AsyncCountdownEvent were necessary for each run so it was left out of contest.
| Description | ParticipantCount | Mean | Ratio | Allocated |
|---|---|---|---|---|
| SignalAndWait · CountdownEvent · Standard | 1 | 6.657 ns | 0.38 | - |
| SignalAndWait · AsyncCountdownEv · RefImpl | 1 | 15.799 ns | 0.90 | 96 B |
| SignalAndWait · AsyncCountdownEv · Pooled | 1 | 17.462 ns | 1.00 | - |
| WaitAndSignal · AsyncCountdownEv · Pooled | 1 | 46.495 ns | 2.66 | - |
| SignalAndWait · CountdownEvent · Standard | 10 | 20.122 ns | 0.40 | - |
| SignalAndWait · AsyncCountdownEv · RefImpl | 10 | 27.920 ns | 0.55 | 96 B |
| SignalAndWait · AsyncCountdownEv · Pooled | 10 | 50.758 ns | 1.00 | - |
| WaitAndSignal · AsyncCountdownEv · Pooled | 10 | 85.093 ns | 1.68 | - |
Benchmark Analysis
Key Findings:
Signal Performance: The
Signal()operation is O(1) until the final signal triggers the broadcast to all waiters.Memory Efficiency: Pooled
IValueTaskSourceinstances reduce allocation pressure in repeated countdown cycles (e.g., usingReset()to reuse the countdown). CancellationTokens do not add memory allocations.Broadcast Overhead: When the count reaches zero, all waiters are signaled individually. For scenarios with many waiters, consider the trade-off compared to shared
Task-based approaches.SignalAndWaitAsync: This combined operation is optimized for the common pattern where a participant signals and then waits for others.
When to Choose AsyncCountdownEvent:
- Fan-out/fan-in coordination patterns
- Batch processing with parallel workers
- Scenarios where the countdown is frequently reset and reused
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
- AsyncBarrier - Async barrier synchronization primitive
- AsyncSemaphore - Async semaphore primitive
- Benchmarks - Benchmark description
© 2026 The Keepers of the CryptoHives