CryptoHives.Foundation.Threading
0.7.37
dotnet add package CryptoHives.Foundation.Threading --version 0.7.37
NuGet\Install-Package CryptoHives.Foundation.Threading -Version 0.7.37
<PackageReference Include="CryptoHives.Foundation.Threading" Version="0.7.37" />
<PackageVersion Include="CryptoHives.Foundation.Threading" Version="0.7.37" />
<PackageReference Include="CryptoHives.Foundation.Threading" />
paket add CryptoHives.Foundation.Threading --version 0.7.37
#r "nuget: CryptoHives.Foundation.Threading, 0.7.37"
#:package CryptoHives.Foundation.Threading@0.7.37
#addin nuget:?package=CryptoHives.Foundation.Threading&version=0.7.37
#tool nuget:?package=CryptoHives.Foundation.Threading&version=0.7.37
π‘οΈ CryptoHives Open Source Initiative π
An open, community-driven collection of cryptography and performance libraries for the .NET ecosystem, maintained by The Keepers of the CryptoHives.
π§΅ CryptoHives.Foundation.Threading
Pooled async synchronization primitives for .NET, built to keep Task and TaskCompletionSource<T> allocations off the hot path.
π§± Classes
Synchronization Primitives
Namespace: CryptoHives.Foundation.Threading.Async.Pooled
| Class | Description |
|---|---|
| AsyncLock | Pooled async mutual exclusion lock |
| AsyncKeyedLock<TKey> | Pooled per-key async exclusive lock (different keys never block each other) |
| AsyncAutoResetEvent | Pooled async auto-reset event (one waiter per signal) |
| AsyncManualResetEvent | Pooled async manual-reset event (all waiters per signal) |
| AsyncSemaphore | Pooled async semaphore with configurable permit count |
| AsyncCountdownEvent | Pooled async countdown event (signals when count reaches zero) |
| AsyncBarrier | Pooled async barrier (synchronizes multiple participants) |
| AsyncReaderWriterLock | Pooled async reader-writer lock (multiple readers or single writer) |
| AsyncConditionVariable | Pooled async condition variable (wait until a condition guarded by an AsyncLock holds) |
| AsyncExchange<T> | Pooled two-party rendezvous that swaps a value between two tasks |
All primitives are backed by ObjectPool<T> and return ValueTask<T>, which keeps per-operation allocations out of high-throughput code paths.
Pooling Support Classes
Namespace: CryptoHives.Foundation.Threading.Pools
| Class | Description | Namespace |
|---|---|---|
IGetPooledManualResetValueTaskSource<T> |
Interface for obtaining pooled IValueTaskSource<T> implementations (providers return PooledManualResetValueTaskSource<T> instances) |
|
ManualResetValueTaskSource<T> |
Abstract base for pooled IValueTaskSource<T> implementations |
|
PooledManualResetValueTaskSource<T> |
Pooled IValueTaskSource<T> implementation with automatic pool return |
|
LocalManualResetValueTaskSource<T> |
Object-local IValueTaskSource<T> without pool integration |
|
PooledValueTaskSourceObjectPolicy<T> |
Object pool policy for PooledManualResetValueTaskSource<T> |
|
ValueTaskSourceObjectPool<T> |
Specialized provider that implements IGetPooledManualResetValueTaskSource<T> and returns pooled task sources |
|
ValueTaskSourceObjectPools |
Static helper with shared pool instances and constants |
Note: This package no longer bundles CryptoHives.Foundation.Threading.Analyzers automatically. Install it separately if you want the Roslyn analyzers alongside the Threading library.
β¨ Key Features
- Pooled primitives β synchronization objects backed by
Microsoft.Extensions.ObjectPool ValueTask-based APIs β minimal to no allocations thanks to object pooling; pooled primitives retain a bounded number of objects, so allocation-free behaviour holds while the workload fits the pool (see Sizing the caches forAsyncKeyedLock<TKey>, the one primitive where the bound is worth sizing deliberately)CancellationTokensupport β full cancellation across all primitives, allocation-free on modern .NETConfigureAwaitsupport β works naturally with.ConfigureAwait(false)in library code- Timeouts β every lock acquisition method accepts a timeout; a timed-out wait throws
TimeoutException, a cancelled one throwsOperationCanceledException - Non-blocking attempts β
AsyncLock.TryLock,AsyncKeyedLock.TryLock,AsyncSemaphore.TryWait,AsyncAutoResetEvent.TryWait,AsyncManualResetEvent.TryWait,AsyncCountdownEvent.TryWait,AsyncReaderWriterLock.TryReaderLock/TryUpgradeableReaderLock/TryWriterLock/Releaser.TryUpgradeToWriterLock, andAsyncExchange<T>.TryExchangeall probe synchronously without allocating an exception or aValueTask - Configurable continuations β control whether continuations run synchronously or asynchronously
- Custom pools β supply your own
IGetPooledManualResetValueTaskSource<T>(orObjectPool<T>) for fine-grained control - Drop-in replacement β swap the namespace, keep the same
using-based patterns - Optional analyzers β ValueTask misuse caught at compile time via the separate
Threading.Analyzerspackage
π₯ Installation
dotnet add package CryptoHives.Foundation.Threading
π‘ Quick Examples
Mutual Exclusion β AsyncLock
using CryptoHives.Foundation.Threading.Async.Pooled;
private readonly AsyncLock _lock = new();
public async Task DoWorkAsync(CancellationToken ct)
{
using (await _lock.LockAsync(ct).ConfigureAwait(false))
{
// Critical section β only one task at a time
await ModifySharedStateAsync().ConfigureAwait(false);
}
}
Per-Key Exclusion β AsyncKeyedLock<TKey>
private readonly AsyncKeyedLock<string> _locksByAccount = new();
public async Task TransferAsync(string accountId, CancellationToken ct)
{
using (await _locksByAccount.LockAsync(accountId, ct).ConfigureAwait(false))
{
// Only one operation per accountId; other accounts proceed in parallel
await ApplyTransferAsync(accountId).ConfigureAwait(false);
}
}
Sizing the caches
AsyncKeyedLock<TKey> is allocation-free while a workload fits inside two caps, and degrades
gracefully β not catastrophically β past either. Both default to 128.
| Cap | Bounds | Past the cap |
|---|---|---|
maxIdleEntries |
key cardinality β the keys that are hot at the same time | Each acquisition of an unmapped key takes over the least recently idled entry and allocates a fresh dictionary node (~48 B/key). The entry itself is still reused. |
maxRetainedWaiters |
simultaneous contention β waiters queued at one moment, summed across all keys | Each waiter beyond the cap is allocated and then discarded rather than returned to the pool. |
A released key stays mapped as an idle entry rather than being torn down, so locking and releasing
the same key repeatedly allocates nothing at all. That is what makes the common case free β and
it is also why maxIdleEntries should span the hot key set rather than the total number of distinct
keys ever seen. Retention is bounded by the keys actually used, so a generous cap costs a lock with
few keys nothing.
Each entry supplies one waiter itself and the pool covers the rest, so a lock with 4 keys and 100 waiters behind each needs 396 pooled waiters β at the default, such a burst allocates on 268 of them.
// Sized to the workload: ~2000 hot tenants, up to ~500 waiters queued at peak
private readonly AsyncKeyedLock<string> _locksByTenant =
new(maxIdleEntries: 2048, maxRetainedWaiters: 512);
Note: the default waiter pool is shared process-wide per closed
TKey, so everyAsyncKeyedLock<string>in the process draws on one budget. PassingmaxRetainedWaitersis also what gives an instance a private pool.
Producer-Consumer β AsyncAutoResetEvent
private readonly AsyncAutoResetEvent _itemReady = new(initialState: false);
private readonly Queue<Item> _queue = new();
// Producer
public void Enqueue(Item item)
{
_queue.Enqueue(item);
_itemReady.Set(); // Releases exactly one waiter
}
// Consumer
public async Task<Item> DequeueAsync(CancellationToken ct)
{
await _itemReady.WaitAsync(ct).ConfigureAwait(false);
return _queue.Dequeue();
}
Broadcast β AsyncManualResetEvent
private readonly AsyncManualResetEvent _ready = new(set: false);
public async Task InitializeAsync()
{
await LoadConfigurationAsync().ConfigureAwait(false);
_ready.Set(); // All waiters are released at once
}
public async Task UseServiceAsync(CancellationToken ct)
{
await _ready.WaitAsync(ct).ConfigureAwait(false);
// Service is initialized
}
Bounded Concurrency β AsyncSemaphore
private readonly AsyncSemaphore _semaphore = new(initialCount: 4);
public async Task FetchAsync(CancellationToken ct)
{
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
try
{
await CallExternalServiceAsync().ConfigureAwait(false);
}
finally
{
_semaphore.Release();
}
}
Fan-Out/Fan-In β AsyncCountdownEvent
private readonly AsyncCountdownEvent _countdown = new(initialCount: 3);
// Each worker signals when its share of the work is done
public void WorkerCompleted() => _countdown.Signal();
// Coordinator waits until every worker has signalled
public async Task WaitForWorkersAsync(CancellationToken ct)
{
await _countdown.WaitAsync(ct).ConfigureAwait(false);
// All 3 workers completed
}
Multi-Phase Synchronization β AsyncBarrier
private readonly AsyncBarrier _barrier = new(participantCount: 3,
postPhaseAction: b => Console.WriteLine($"Phase {b.CurrentPhase} completed by all participants"));
public async Task ParticipantWorkAsync(int participantId, CancellationToken ct)
{
await DoPhase1WorkAsync(participantId).ConfigureAwait(false);
// Blocks until every participant has arrived; the post-phase action then
// runs once before all participants are released together
await _barrier.SignalAndWaitAsync(ct).ConfigureAwait(false);
await DoPhase2WorkAsync(participantId).ConfigureAwait(false);
}
Read-Write Separation β AsyncReaderWriterLock
private readonly AsyncReaderWriterLock _rwLock = new();
public async Task<Data> ReadAsync(CancellationToken ct)
{
using (await _rwLock.ReaderLockAsync(ct).ConfigureAwait(false))
return _cache.Get();
}
public async Task WriteAsync(Data data, CancellationToken ct)
{
using (await _rwLock.WriterLockAsync(ct).ConfigureAwait(false))
_cache.Set(data);
}
Wait-Until-Condition β AsyncConditionVariable
Pairs with an AsyncLock: a wait atomically releases the lock, suspends, and re-acquires it before
returning β on every return path, including cancellation. Any number of callers can wait for a
shared resource to reach a specific state; the condition is arbitrary state protected by the lock:
private readonly AsyncLock _lock = new();
private readonly AsyncConditionVariable _ready = new();
private ConnectionState _state = ConnectionState.Disconnected;
// The reconnect loop is the only writer of _state.
private async Task ReconnectLoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
await ConnectAsync(ct).ConfigureAwait(false);
using (await _lock.LockAsync(ct).ConfigureAwait(false))
{
_state = ConnectionState.Ready;
_ready.SignalAll(); // every caller waiting for readiness wakes up
}
await RunUntilDisconnectedAsync(ct).ConfigureAwait(false);
}
finally
{
using (await _lock.LockAsync(CancellationToken.None).ConfigureAwait(false))
_state = ConnectionState.Disconnected;
}
}
}
// Any caller can wait for the connection to become ready before using it.
public async Task<T> SendAsync<T>(Func<Task<T>> operation, CancellationToken ct)
{
using (await _lock.LockAsync(ct).ConfigureAwait(false))
{
while (_state != ConnectionState.Ready)
await _ready.WaitAsync(_lock, ct).ConfigureAwait(false);
}
return await operation().ConfigureAwait(false); // outside the lock
}
Two-Party Rendezvous β AsyncExchange<T>
Two peer workers, doing the same job, meeting at the rendezvous every round to swap their current
item for the other's β neither is a producer or a consumer, and nothing is ever buffered between
rounds. This is the async counterpart of Java's Exchanger<V>, which the BCL has no equivalent of.
private readonly AsyncExchange<Item> _rendezvous = new();
public async Task RunPeerAsync(CancellationToken ct)
{
Item mine = ComputeNextItem();
while (!ct.IsCancellationRequested)
{
Item theirs = await _rendezvous.ExchangeAsync(mine, ct).ConfigureAwait(false);
CrossCheck(mine, theirs);
mine = ComputeNextItem();
}
}
// Both peers run the exact same method - there is no asymmetric role to assign.
_ = Task.Run(() => RunPeerAsync(ct));
_ = Task.Run(() => RunPeerAsync(ct));
Non-Blocking Attempts β Try*
Every acquiring primitive exposes a synchronous Try* probe that returns false instead of throwing
TimeoutException on a miss β no exception, no faulted ValueTask, nothing to await. Use them on paths
that shed work rather than queue it.
// Opportunistic cache rebuild β skipped entirely if the lock is busy
if (_rwLock.TryWriterLock(out var releaser))
{
using (releaser)
{
RebuildCache();
}
}
// Rate limiter that reports "busy" instead of awaiting
if (_permits.TryWait())
{
try { Handle(request); }
finally { _permits.Release(); }
}
else
{
Reject(request);
}
AsyncAutoResetEvent.TryWait() consumes the signal (like a completed WaitAsync()), whereas
AsyncManualResetEvent.TryWait() and AsyncCountdownEvent.TryWait() are non-consuming aliases of IsSet.
A failed releaser-returning Try* hands back default β never dispose it.
Custom Pool
using CryptoHives.Foundation.Threading.Pools;
var policy = new PooledValueTaskSourceObjectPolicy<bool>();
var pool = new ValueTaskSourceObjectPool<bool>(policy, maximumRetained: 64);
var evt = new AsyncAutoResetEvent(
initialState: false,
runContinuationAsynchronously: true,
pool: pool);
π ValueTask Contract
- Await a
ValueTaskexactly once. A secondawaitorAsTask()call may throwInvalidOperationException. - Avoid calling
AsTask()before the primitive signals. WithRunContinuationsAsynchronously=true(the default), storing the result ofAsTask()too early causes a severe performance hit. Await theValueTaskdirectly wherever you can. - Always await or discard a waiter. If it's left unconsumed, the underlying
IValueTaskSourcenever makes it back to the pool.
The separate Threading.Analyzers package enforces these rules at compile time.
π Documentation
| Resource | Link |
|---|---|
| Full package documentation | cryptohives.github.io/Foundation/packages/threading |
| API reference | cryptohives.github.io/β¦/api/β¦Threading.Async.Pooled |
| Benchmarks | cryptohives.github.io/Foundation/packages/threading/benchmarks |
| Threading.Analyzers | cryptohives.github.io/Foundation/packages/threading.analyzers |
| Source repository | github.com/CryptoHives/Foundation |
π¨ Security Policy
If you discover a vulnerability, please don't open a public issue β follow the process on the CryptoHives Security Page instead.
βοΈ License
MIT β Β© 2026 The Keepers of the CryptoHives
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 is compatible. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.6.2
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.12)
- Microsoft.Bcl.HashCode (>= 6.0.0)
- Microsoft.Bcl.TimeProvider (>= 10.0.12)
- Microsoft.Extensions.ObjectPool (>= 10.0.12)
- System.Memory (>= 4.6.3)
- System.Threading.Tasks.Extensions (>= 4.6.3)
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.12)
- Microsoft.Bcl.HashCode (>= 6.0.0)
- Microsoft.Bcl.TimeProvider (>= 10.0.12)
- Microsoft.Extensions.ObjectPool (>= 10.0.12)
- System.Memory (>= 4.6.3)
- System.Threading.Tasks.Extensions (>= 4.6.3)
-
.NETStandard 2.1
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.12)
- Microsoft.Bcl.TimeProvider (>= 10.0.12)
- Microsoft.Extensions.ObjectPool (>= 10.0.12)
-
net10.0
- Microsoft.Extensions.ObjectPool (>= 10.0.12)
-
net8.0
- Microsoft.Extensions.ObjectPool (>= 10.0.12)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.7.37 | 0 | 9/30/2026 |
| 0.6.101 | 304 | 9/1/2026 |
| 0.6.79 | 263 | 8/12/2026 |
| 0.6.51 | 240 | 8/1/2026 |
| 0.6.21 | 296 | 7/4/2026 |
| 0.5.34-preview | 649 | 6/2/2026 |
| 0.5.21-preview | 173 | 5/2/2026 |
| 0.5.13-preview | 128 | 4/2/2026 |
| 0.4.21-preview | 138 | 3/1/2026 |
| 0.4.11-preview | 137 | 2/14/2026 |
| 0.3.19-preview | 138 | 1/26/2026 |
| 0.2.43-preview | 142 | 1/9/2026 |
| 0.2.33-preview | 484 | 12/9/2025 |
| 0.2.30-preview | 392 | 12/8/2025 |
| 0.2.28-preview | 329 | 12/7/2025 |
| 0.2.26-preview | 272 | 12/6/2025 |
| 0.2.22-preview | 625 | 12/1/2025 |
| 0.2.17-preview | 230 | 11/23/2025 |
| 0.2.13-preview | 458 | 11/20/2025 |