Table of Contents

Class AsyncConditionVariable

Namespace
CryptoHives.Foundation.Threading.Async.Pooled
Assembly
CryptoHives.Foundation.Threading.dll

An async condition variable that pairs with AsyncLock to provide "wait until condition" semantics without blocking a thread, equivalent to Wait(object) for async code.

public sealed class AsyncConditionVariable : IResettable
Inheritance
AsyncConditionVariable
Implements
Inherited Members

Remarks

A condition variable must always be used in conjunction with an AsyncLock. The caller must hold the lock when calling WaitAsync(AsyncLock, CancellationToken). The wait atomically releases the lock and suspends the caller until Signal() or SignalAll() is called, after which it re-acquires the lock before returning. The caller is guaranteed to hold the lock on every return path, including when an exception is thrown.

An instance binds to the first AsyncLock it is used with. Passing a different lock to a later wait throws an InvalidOperationException: a condition variable shared across two locks cannot make the atomic release/re-acquire guarantee, and the resulting corruption is otherwise silent. The binding is cleared by TryReset().

Unlike AsyncManualResetEvent and AsyncAutoResetEvent, a signal that fires when no waiters are present is not stored - it is lost. Always use a while loop to re-check the predicate after returning from a wait:

private readonly AsyncLock _lock = new AsyncLock();
private readonly AsyncConditionVariable _ready = new AsyncConditionVariable();
private bool _hasItem;

public async Task ProduceAsync(CancellationToken ct) { using (await _lock.LockAsync(ct)) { _hasItem = true; _ready.Signal(); } }

public async Task ConsumeAsync(CancellationToken ct) { using (await _lock.LockAsync(ct)) { while (!_hasItem) await _ready.WaitAsync(_lock, ct); _hasItem = false; } }

Cancellation and timeout bound the wait for a signal, not the re-acquisition of the lock. Once a waiter has consumed a signal it always completes successfully, even if the token is cancelled or the timeout elapses while it is queued behind the lock. Reporting cancellation at that point would swallow the signal the waiter has already taken and the next waiter would never be woken. Cancelling therefore only fails a wait that has not been signalled; a caller that must observe cancellation promptly does so in its own predicate loop, which is where the token belongs anyway.

Allocation behaviour: the first concurrent waiter uses an instance-local IValueTaskSource<TResult> and further concurrent waiters are served from a pool, so waiting allocates nothing on .NET 6.0+ beyond the state machine described below. Specifying a finite timeout allocates a timer that is disposed when the wait completes.

Note: the wait is an async method, so it boxes a state machine when it suspends: it awaits two things in sequence, the signal and then the re-acquisition of the lock. Removing that allocation means transferring a signalled waiter directly into the lock's own wait queue instead of completing it and re-acquiring - a deliberate future change rather than an oversight.

The IResettable interface is implemented to allow resetting the state of the instance for reuse by an ObjectPool<T> using the DefaultObjectPool<T> implementation.

Constructors

AsyncConditionVariable(bool, IGetPooledManualResetValueTaskSource<bool>?)

Constructs a new AsyncConditionVariable.

public AsyncConditionVariable(bool runContinuationAsynchronously = true, IGetPooledManualResetValueTaskSource<bool>? pool = null)

Parameters

runContinuationAsynchronously bool

When true (default), continuations are forced to the thread pool when a signal is received, preventing the signaling thread from being hijacked.

pool IGetPooledManualResetValueTaskSource<bool>

Custom pool for waiter instances.

Properties

RunContinuationAsynchronously

Gets or sets whether continuations are forced to run asynchronously after a signal.

public bool RunContinuationAsynchronously { get; set; }

Property Value

bool

WaiterCount

Gets the number of tasks currently waiting for a signal.

public int WaiterCount { get; }

Property Value

int

Methods

Signal()

Wakes one waiting task. If no tasks are waiting, the signal is lost.

public void Signal()

SignalAll()

Wakes all waiting tasks. If no tasks are waiting, the signal is lost.

public void SignalAll()

TryReset()

Reset the object to a neutral state, semantically similar to when the object was first constructed.

public bool TryReset()

Returns

bool

true if the object was able to reset itself, otherwise false.

Remarks

In general, this method is not expected to be thread-safe.

WaitAsync(AsyncLock, CancellationToken)

Atomically releases asyncLock and waits for a signal, then re-acquires asyncLock before returning. The caller must hold asyncLock on entry and will always hold it on return, including when an exception is thrown.

public ValueTask WaitAsync(AsyncLock asyncLock, CancellationToken cancellationToken = default)

Parameters

asyncLock AsyncLock

The lock to release while waiting and re-acquire after signaling.

cancellationToken CancellationToken

Token to cancel the wait. A wait that has already consumed a signal completes successfully; see the type remarks. The lock is re-acquired before the resulting OperationCanceledException is propagated.

Returns

ValueTask

A ValueTask that completes when the caller has been signalled and holds the lock again.

Exceptions

ArgumentNullException

Thrown when asyncLock is null.

InvalidOperationException

Thrown when this instance has already been used with a different AsyncLock.

SynchronizationLockException

Thrown when asyncLock is not held on entry.

OperationCanceledException

Thrown when cancellationToken is cancelled before a signal is received. The lock is always re-acquired before this exception propagates.

WaitAsync(AsyncLock, TimeSpan, CancellationToken)

Atomically releases asyncLock and waits for a signal or for timeout to elapse, then re-acquires asyncLock before returning. The caller must hold asyncLock on entry and will always hold it on return, including when an exception is thrown.

public ValueTask WaitAsync(AsyncLock asyncLock, TimeSpan timeout, CancellationToken cancellationToken = default)

Parameters

asyncLock AsyncLock

The lock to release while waiting and re-acquire after signaling.

timeout TimeSpan

The maximum time to wait for a signal. Use InfiniteTimeSpan to wait indefinitely. The timeout bounds the wait for the signal only, not the re-acquisition of the lock. Zero always times out: a condition variable holds no state that a zero-length wait could observe.

cancellationToken CancellationToken

Token to cancel the wait. A wait that has already consumed a signal completes successfully; see the type remarks.

Returns

ValueTask

A ValueTask that completes when the caller has been signalled and holds the lock again.

Exceptions

ArgumentNullException

Thrown when asyncLock is null.

ArgumentOutOfRangeException

Thrown when timeout is negative and not equal to InfiniteTimeSpan.

InvalidOperationException

Thrown when this instance has already been used with a different AsyncLock.

SynchronizationLockException

Thrown when asyncLock is not held on entry.

TimeoutException

Thrown when timeout elapses before a signal is received. The lock is always re-acquired before this exception propagates.

OperationCanceledException

Thrown when cancellationToken is cancelled before a signal is received. The lock is always re-acquired before this exception propagates.