AsyncSemaphore
A pooled, allocation-free async semaphore that limits concurrent access using ValueTask-based waiters.
Overview
AsyncSemaphore is an async-compatible semaphore that allows a configurable number of concurrent operations. It uses pooled IValueTaskSource instances to minimize allocations in high-throughput scenarios.
Usage
Basic Usage
using CryptoHives.Foundation.Threading.Async.Pooled;
private readonly AsyncSemaphore _semaphore = new AsyncSemaphore(3);
public async Task AccessLimitedResourceAsync(CancellationToken ct)
{
await _semaphore.WaitAsync(ct);
try
{
// Max 3 concurrent tasks can access this section
await AccessResourceAsync();
}
finally
{
_semaphore.Release();
}
}
Rate Limiting
private readonly AsyncSemaphore _rateLimiter = new AsyncSemaphore(5);
public async Task<T> RateLimitedOperationAsync<T>(Func<Task<T>> operation, CancellationToken ct)
{
await _rateLimiter.WaitAsync(ct);
try
{
return await operation();
}
finally
{
_rateLimiter.Release();
}
}
Constructor
public AsyncSemaphore(
int initialCount,
bool runContinuationAsynchronously = true,
IGetPooledManualResetValueTaskSource<bool>? pool = null)
| Parameter | Description |
|---|---|
initialCount |
The initial number of available permits. Must be non-negative. |
runContinuationAsynchronously |
If true (default), continuations run on the thread pool. |
pool |
Optional custom pool for ValueTaskSource instances. |
Properties
| Property | Type | Description |
|---|---|---|
CurrentCount |
int |
Gets the current number of available permits. |
RunContinuationAsynchronously |
bool |
Gets or sets whether continuations run asynchronously. |
Methods
WaitAsync
public ValueTask WaitAsync(CancellationToken cancellationToken = default)
Asynchronously waits to acquire a permit from the semaphore.
Release
public void Release()
public void Release(int releaseCount)
Releases one or more permits back to the semaphore.
Performance
- O(1) acquisition when permits are available
- FIFO queue for waiters when no permits are available
- Zero allocations on the fast path (uncontended)
- Pooled ValueTaskSource instances for waiters
Benchmark Results
The following benchmarks compare AsyncSemaphore against SemaphoreSlim, Nito.AsyncEx.AsyncSemaphore and a Task based reference implementation.
TODO: Currently benchmarks are only available on uncontended scenarios to measure the overhead of a semaphore acquisition and release.
Single Wait/Release Benchmark
Measures the performance of acquiring and releasing a single permit.
| Description | Mean | Ratio | Allocated |
|---|---|---|---|
| WaitRelease · AsyncSemaphore · Pooled | 15.38 ns | 1.00 | - |
| WaitRelease · AsyncSemaphore · Nito.AsyncEx | 16.50 ns | 1.07 | - |
| WaitRelease · SemaphoreSlim · SemaphoreSlim | 17.30 ns | 1.12 | - |
| WaitRelease · AsyncSemaphore · RefImpl | 17.35 ns | 1.13 | - |
Benchmark Analysis
Key Findings:
Fast Path Performance: When permits are available,
AsyncSemaphorecompletes synchronously with zero allocations, providing excellent performance for uncontended scenarios.Contention Handling: Under contention, the pooled
IValueTaskSourceapproach reduces allocation pressure compared toSemaphoreSlim.WaitAsync().Memory Efficiency: The local waiter optimization ensures the first queued waiter incurs no allocation, with subsequent waiters benefiting from pool reuse.
ValueTask Advantage: Returning
ValueTaskinstead ofTaskavoids allocations when permits are immediately available.
When to Choose AsyncSemaphore:
- Rate limiting scenarios with frequent permit acquisition
- Connection pooling where semaphore operations are hot paths
- High-throughput scenarios where allocation pressure matters
Comparison with SemaphoreSlim:
SemaphoreSlim is a mature, well-tested synchronization primitive in the .NET BCL. Choose AsyncSemaphore when:
- You need lower allocation overhead in high-frequency scenarios
- Your application is allocation-sensitive
- You want consistent
ValueTask-based APIs across your codebase
Comparison with SemaphoreSlim
| Feature | AsyncSemaphore | SemaphoreSlim |
|---|---|---|
| Allocation overhead | Minimal (pooled) | Higher (per wait) |
| ValueTask support | Native | Via WaitAsync |
| Cancellation | Full support | Full support |
| Performance | Optimized | Standard |
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
- AsyncBarrier - Async barrier synchronization primitive
- Benchmarks - Benchmark description
© 2026 The Keepers of the CryptoHives