Bodu.Threading Namespace
- Package
-
Bodu.Core 1.0.1
Purpose
Bodu.Threading is a set of asynchronous coordination primitives for async/await code - the awaitable counterparts of the BCL synchronous gates in System.Threading. Each type lets a caller wait on a condition without blocking a thread: a held lock can span an await, a signal can be awaited, and a burst of triggers can be coalesced or rate-limited. The primitives own no operating-system handles (those that need explicit teardown implement IDisposable only to fault still-queued waiters at shutdown).
The whole namespace follows two shared conventions. First, acquisition is exposed through a ValueTask that must be awaited exactly once, and a contention-free acquisition completes synchronously with no allocation. Second, success wins over cancellation: an acquisition that can be granted immediately is granted even when the supplied CancellationToken is already canceled - the token only cancels an acquisition that must queue, and cancellation removes only the calling waiter. Waiters that do queue are released in strict FIFO order, with continuations scheduled asynchronously so a releasing thread is never hijacked to run a waiter inline.
Static documentation
- Introduction - the Bodu.Core headline types and the scenarios the library covers.
- Async coordination primitives - each primitive mapped to its synchronous BCL analogue, with a compiling pattern per type.
Key types
Locks and gates
- AsyncLock - a non-reentrant async mutex.
LockAsync()returns a AsyncLock.Releaser whose disposal releases the lock; scope it withusing (await gate.LockAsync()). Implements IDisposable. - AsyncSemaphore - an async counting semaphore for bounded concurrency.
WaitAsync()/Release()for manual permit management, orLockAsync()for a disposable AsyncSemaphore.Releaser; ctors take(initialCount)or(initialCount, maxCount). - AsyncReaderWriterLock - a writer-preferring reader/writer lock.
ReaderAsync()admits many concurrent readers;WriterAsync()grants one exclusive writer. Both yield an idempotent AsyncReaderWriterLock.Releaser. Implements IDisposable. - RateGate - a synchronous leading-edge admission gate.
TryInvoke()returns whether a call is admitted this interval;TimeUntilNextreports the remaining cool-down.
Events
- AsyncAutoResetEvent - auto-reset signal: each
Set()releases exactly one waiter and reverts to unsignaled (latching at most one pending signal). Wait withWaitAsync(). - AsyncManualResetEvent - manual-reset gate:
Set()releases all current and future waiters,Reset()closes the gate again,IsSetreports state. - AsyncCountdownEvent - fan-in countdown:
Signal()decrements the count,AddCount()/TryAddCount()raise it, andWaitAsync()completes once the count reaches zero. Not resettable.
Lazy initialization
- AsyncLazy<T> - runs an initializer at most once and caches the resulting Task<TResult>, shared by every awaiter. Awaitable directly (
await lazy) or viaValue/GetValueAsync(token); constructed from aFunc<T>(offloaded to the thread pool) or aFunc<Task<T>>.
Coalescing and rate limiting
- AsyncDebouncer - coalesces a burst of
Invoke()triggers into a single callback that runs once a quiet period elapses.FlushAsync()runs a pending invocation now,Cancel()discards it,DrainAsync()awaits in-flight work; overlap behavior is set by AsyncDebouncerExecutionPolicy. Implements IDisposable. - AsyncDebouncerExecutionPolicy - the overlap policy:
QueueOneTrailingRun(default),DropWhileRunning,CancelAndRestart,AllowOverlap.
Example
using Bodu.Threading;
// Mutual exclusion: a held lock may span an await.
private readonly AsyncLock _mutex = new();
public async Task UpdateAsync()
{
using (await _mutex.LockAsync())
{
// Exclusive section; safe to await here.
await SomeOperationAsync();
}
}
using Bodu.Threading;
// Bounded concurrency: at most four operations run at once.
private readonly AsyncSemaphore _throttle = new(initialCount: 4);
public async Task DownloadAsync(Uri uri)
{
using (await _throttle.LockAsync())
{
await HttpGetAsync(uri);
}
}
Notes
- Await exactly once. Every
WaitAsync/LockAsync/ReaderAsync/WriterAsyncreturns aValueTask; await it a single time. An uncontended acquisition completes synchronously and allocates nothing. - Success wins over cancellation. A free lock, an available permit, or a latched signal is taken even when the token passed to
…Async(token)is already canceled. The token only cancels an acquisition that must queue. - FIFO ordering. AsyncLock, AsyncSemaphore, and AsyncAutoResetEvent release queued waiters in strict first-in, first-out order. AsyncReaderWriterLock is writer-preferring: queued writers go FIFO and readers are admitted in a batch when no writer is queued.
- Not reentrant. AsyncLock and AsyncReaderWriterLock deadlock if the same flow re-acquires access it already holds; the reader/writer lock also has no upgradeable mode.
- Releasers dispose once. A AsyncLock.Releaser or AsyncSemaphore.Releaser should be disposed exactly once; the AsyncReaderWriterLock.Releaser is idempotent and tolerates repeat disposal.
- Disposal is for shutdown. Disposing AsyncLock, AsyncReaderWriterLock, or AsyncDebouncer faults any still-queued waiter with ObjectDisposedException; dispose only when no further acquisitions are expected. The event types and AsyncSemaphore own no handle and are not IDisposable.
- Testable timing. AsyncDebouncer and RateGate schedule through a TimeProvider (defaulting to System), so timing-dependent behavior can be driven deterministically in tests.
- See also: the async coordination primitives guide and the Bodu.Core introduction.
Classes
- AsyncAutoResetEvent
Provides an asynchronous auto-reset signaling primitive: each call to Set() releases exactly one waiter and then automatically returns to the unsignaled state.
- AsyncCountdownEvent
Provides an asynchronous countdown synchronization primitive that becomes signaled once its count reaches zero, releasing all waiters.
- AsyncDebouncer
Coalesces a rapid burst of triggers into a single asynchronous invocation that runs once a quiet period has elapsed since the most recent trigger.
- AsyncLazy<T>
Provides support for asynchronous lazy initialization: a value is produced at most once, on first access, and the resulting task is cached and shared by every awaiter.
- AsyncLock
Provides an asynchronous, non-reentrant mutual-exclusion primitive whose acquisition can be awaited without blocking a thread.
- AsyncManualResetEvent
Provides an asynchronous, manually reset signaling primitive: once set, every current and future waiter is released until the event is explicitly reset.
- AsyncReaderWriterLock
Provides an asynchronous, writer-preferring reader/writer lock whose acquisitions can be awaited without blocking a thread.
- AsyncSemaphore
Provides a lightweight asynchronous counting semaphore that admits a bounded number of concurrent holders and releases waiters in strict first-in, first-out order.
- RateGate
Provides a synchronous, leading-edge admission gate that admits at most one invocation per fixed interval, dropping any calls that arrive while the cool-down window opened by the previous admitted call is still open.
Structs
- AsyncLock.Releaser
Represents an acquired AsyncLock. Disposing the releaser releases the lock.
- AsyncReaderWriterLock.Releaser
Represents acquired access to an AsyncReaderWriterLock. Disposing the releaser releases the read or write access it represents.
- AsyncSemaphore.Releaser
Represents a permit taken from an AsyncSemaphore. Disposing the releaser returns the permit.
Enums
- AsyncDebouncerExecutionPolicy
Specifies how an AsyncDebouncer behaves when a debounced run becomes due while a previous callback invocation is still in flight.