Table of Contents

Class AsyncAutoResetEvent

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

An async version of AutoResetEvent which uses a pooled approach to implement waiters for ValueTask to reduce memory allocations.

public sealed class AsyncAutoResetEvent : IResettable
Inheritance
AsyncAutoResetEvent
Implements
Inherited Members

Remarks

This implementation uses ValueTask for waiters and provides allocation-free async signaling by reusing pooled IValueTaskSource<TResult> instances to avoid allocations of TaskCompletionSource<TResult> and Task. As a side effect, the benchmarks reveal that in many cases performance is also better than other known implementations.

Optional timeout and cancellation token parameters on WaitAsync(TimeSpan, CancellationToken).

Important Usage Note: Awaiting on ValueTask has its own caveats, as it is a struct that can only be awaited or converted with AsTask() ONE single time. Additional attempts to await after the first await or additional conversions to AsTask() will throw an InvalidOperationException.

Continuation Scheduling: The RunContinuationAsynchronously property controls how continuations are executed when the event is signaled. When set to true (default), continuations are forced to queue to the thread pool, preventing the signaling thread from being blocked by continuation execution. When set to false, continuations may execute synchronously on the signaling thread, reducing context switching overhead but potentially increasing Set() call duration and could lead to deadlocks because the waiting code may be executed directly by the signaling thread.

Allocation Behavior: Immediate acquisitions are completely allocation-free using atomic operations. When the lock is contended, waiting without a timeout is allocation-free on .NET 6.0+ (using UnsafeRegister for cancellation), while older frameworks may allocate for cancellation registration. Specifying a finite timeout allocates a timer that is automatically disposed when the operation completes. Exception and task allocations occur only if a timeout actually elapses or cancellation is triggered; successful acquisitions are otherwise allocation-free. Pooled IValueTaskSource<TResult> instances are reused to minimize allocation pressure across repeated lock operations.

Performance Warning - AsTask() Usage: When RunContinuationAsynchronously is true, converting the returned ValueTask to Task via AsTask() and storing the result before the event is signaled can cause severe performance degradation (often 10x-100x slower) and additional memory allocations. This is an implementation detail of the underlying ManualResetValueTaskSourceCore<TResult> which cannot complete synchronously when a Task wrapper exists before signaling, forcing asynchronous scheduling even when the event is already signaled.

Recommendation: Always await ValueTask directly. Avoid conversion to Task using AsTask(). Avoid storing AsTask() results across signaling boundaries. When usage of Task is mandatory, a native implementation using TaskCompletionSource<TResult> should be considered.

// GOOD: Direct ValueTask await (optimal performance)
await eventInstance.WaitAsync().ConfigureAwait(false);

// SIGH: Immediate AsTask() conversion and await (adds memory allocation, but avoids performance hit) await eventInstance.WaitAsync().AsTask().ConfigureAwait(false);

// BAD: Storing AsTask before signaling (possible severe performance degradation) Task t = eventInstance.WaitAsync().AsTask(); // Stored before Set() eventInstance.Set(); ... await t.ConfigureAwait(false); // 10x-100x slower due to forced async scheduling AFTER Set()

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

Constructors

AsyncAutoResetEvent(bool, bool, IGetPooledManualResetValueTaskSource<bool>?)

Constructs a new AsyncLock instance with optional custom pool and custom default queue size.

public AsyncAutoResetEvent(bool initialState = false, bool runContinuationAsynchronously = true, IGetPooledManualResetValueTaskSource<bool>? pool = null)

Parameters

initialState bool

The initial state of the event.

runContinuationAsynchronously bool

Indicates if continuations are forced to run asynchronously.

pool IGetPooledManualResetValueTaskSource<bool>

Custom pool for this instance.

Properties

IsSet

Whether this event is currently set.

public bool IsSet { get; }

Property Value

bool

RunContinuationAsynchronously

Gets or sets whether to force continuations to run asynchronously.

public bool RunContinuationAsynchronously { get; set; }

Property Value

bool

Remarks

When true (default), continuations are queued to the thread pool when the event is signaled, preventing the signaling thread from being blocked by continuation execution. When false, continuations may execute synchronously on the signaling thread.

Performance Warning: When this property is true, converting ValueTask to Task via AsTask() and storing the result before signaling causes severe performance degradation. Always await ValueTask directly to avoid this issue.

Methods

PulseAll()

Signals all waiting tasks to complete successfully.

public void PulseAll()

Set()

Signals the event, releasing a single waiting thread if any are queued.

public void Set()

Remarks

If no threads are waiting, the event is set to a signaled state, allowing any subsequent threads to proceed without blocking. This method is thread-safe.

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(CancellationToken)

Asynchronously waits for a signal to be received.

public ValueTask WaitAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

The cancellation token used to cancel the wait.

Returns

ValueTask

A ValueTask that is used for the asynchronous wait operation.

Remarks

If the signal has already been received, the method returns a completed ValueTask. Otherwise, it enqueues a waiter and returns a task that completes when the signal is received. The ValueTask is a struct that can only be awaited or transformed with AsTask() ONE time, then it is returned to the pool and every subsequent access throws an InvalidOperationException.

var event = new AsyncAutoResetEvent();

// GOOD: single await
await _event.WaitAsync().ConfigureAwait(false);

// GOOD: single await after calling WaitAsync()
ValueTask vt = _event.WaitAsync();
_event.Set();
await vt.ConfigureAwait(false);

// FAIL: multiple awaits on ValueTask - throws InvalidOperationException on second await
await vt.ConfigureAwait(false);

// GOOD: single AsTask() usage, multiple await on Task
Task t = _event.WaitAsync().AsTask();
_event.Set();
await t.ConfigureAwait(false);
await t.ConfigureAwait(false);

// FAIL: single await with GetAwaiter().GetResult() - is undefined behavior and
// may throw InvalidOperationException. Convert to Task first.
await _event.WaitAsync().GetAwaiter().GetResult();

Be aware that the underlying pooled implementation of IValueTaskSource may leak if the returned ValueTask is never awaited or transformed to a Task.

WaitAsync(TimeSpan, CancellationToken)

Asynchronously waits for a signal to be received, or until the specified timeout elapses.

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

Parameters

timeout TimeSpan

The maximum time to wait. Use InfiniteTimeSpan to wait indefinitely.

cancellationToken CancellationToken

A cancellation token that can be used to cancel the attempt to wait for an event.

Returns

ValueTask

A ValueTask that completes when the signal is received.

Remarks

If the signal has already been received, the method returns a completed ValueTask immediately without allocating any cancellation infrastructure. A CancellationTokenSource is allocated only when the event is not already signalled and a finite positive timeout is requested; it is disposed automatically when the returned ValueTask is awaited.

Exceptions

ArgumentOutOfRangeException

Thrown when timeout is negative and not equal to InfiniteTimeSpan.

TimeoutException

Thrown when the timeout elapses before the event is signalled.

OperationCanceledException

Thrown when cancellationToken is cancelled before the event is signalled.