Class AsyncManualResetEvent
- Namespace
- CryptoHives.Foundation.Threading.Async.Pooled
- Assembly
- CryptoHives.Foundation.Threading.dll
An async version of ManualResetEvent which uses a pooled approach to implement waiters for ValueTask to reduce memory allocations.
public sealed class AsyncManualResetEvent : IResettable
- Inheritance
-
AsyncManualResetEvent
- 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. However, since every waiter needs its own IValueTaskSource<TResult>, there is more overhead when many waiters are signalled than with a native TaskCompletionSource<TResult> implementation.
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
AsyncManualResetEvent(bool, bool, IGetPooledManualResetValueTaskSource<bool>?)
Creates an async ValueTask compatible ManualResetEvent.
public AsyncManualResetEvent(bool set = false, bool runContinuationAsynchronously = true, IGetPooledManualResetValueTaskSource<bool>? pool = null)
Parameters
setboolThe initial state of the event.
runContinuationAsynchronouslyboolIndicates if continuations are forced to run asynchronously.
poolIGetPooledManualResetValueTaskSource<bool>Custom pool for this instance.
Properties
IsSet
Whether this event is currently set.
public bool IsSet { get; }
Property Value
RunContinuationAsynchronously
Gets or sets whether to force continuations to run asynchronously.
public bool RunContinuationAsynchronously { get; set; }
Property Value
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
Reset()
Resets the event. If the event is already reset, this method does nothing.
public void Reset()
Set()
Signals the event, releasing all waiting threads 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
Remarks
In general, this method is not expected to be thread-safe.
WaitAsync(CancellationToken)
Asynchronously waits for this event to be set.
public ValueTask WaitAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationTokenThe cancellation token used to cancel the wait.
Returns
Remarks
If the event is already signalled, 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 ct = new CancellationTokenSource(1000).Token;
var event = new AsyncManualResetEvent();
// GOOD: single await with cancellation token
await _event.WaitAsync(ct).ConfigureAwait(false);
// GOOD: single await after calling WaitAsync()
ValueTask vt = _event.WaitAsync(ct);
_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(ct).AsTask();
_event.Set();
await t.ConfigureAwait(false);
await t.ConfigureAwait(false);
// FAIL: single await with GetAwaiter().GetResult() - may throw InvalidOperationException
await _event.WaitAsync(ct).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 the event to be set, or until the specified timeout elapses.
public ValueTask WaitAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
Parameters
timeoutTimeSpanThe maximum time to wait. Use InfiniteTimeSpan to wait indefinitely.
cancellationTokenCancellationTokenThe cancellation token used to cancel the wait.
Returns
Remarks
If the event is already signalled, 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
timeoutis negative and not equal to InfiniteTimeSpan.- TimeoutException
Thrown when the timeout elapses before the event is set.
- OperationCanceledException
Thrown when
cancellationTokenis cancelled before the event is set.