Class AsyncReaderWriterLock
- Namespace
- CryptoHives.Foundation.Threading.Async.Pooled
- Assembly
- CryptoHives.Foundation.Threading.dll
An async reader-writer lock which uses a pooled approach to implement waiters for ValueTask to reduce memory allocations. Supports optional timeout and cancellation token parameters on all lock acquisition methods.
public sealed class AsyncReaderWriterLock : IResettable
- Inheritance
-
AsyncReaderWriterLock
- 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.
The lock supports multiple concurrent readers, concurrent readers with an upgradeable reader, a single exclusive writer or a single upgraded exclusive writer. Writer and upgraded writer requests are prioritized over new reader requests to prevent writer starvation. New readers are released in batches to avoid excessive Task wakeup.
------------
| | <-----> READERS
| | <-----> UPGRADEABLE READER + READERS
| IDLE | <-----> UPGRADEABLE READER -----> UPGRADED WRITER --\
| NO LOCKS | ^ |
| | |------- DEMOTE TO UPGRADEABLE READER <--/
| | <--------------- DEMOTE TO IDLE WITHOUT READER <--/
| | <-----> WRITER
------------
Each state employs a queue for waiting tasks, and the transitions between states are managed through atomic operations and a lock to ensure thread safety.
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 lock is released. When set to true (default), continuations are forced to queue to the thread pool.
Allocation Behavior: Lock acquisition is optimized for minimal allocations:
- Immediate acquisition (fast path): Completely allocation-free using atomic operations.
-
Waiting with cancellation token: Allocation-free on .NET 6.0+ when using cancellation tokens
(via
UnsafeRegister). On older frameworks, a cancellation registration allocation may occur. - Waiting with timeout: A timer is allocated when the lock cannot be acquired immediately and a finite timeout is specified. The timer is automatically disposed when the operation completes.
- On timeout or cancellation: An exception and task wrapper are allocated only if the wait is actually cancelled or times out. Successful acquisitions incur no additional allocations.
Pooled IValueTaskSource<TResult> instances are reused to minimize allocation pressure for repeated lock operations.
private readonly AsyncReaderWriterLock _rwLock = new AsyncReaderWriterLock();
public async Task ReadAsync(CancellationToken ct)
{
using (await _rwLock.ReaderLockAsync(ct))
{
// Multiple readers can hold the lock concurrently
await ReadDataAsync();
}
}
public async Task WriteAsync(TimeSpan timeout, CancellationToken ct)
{
using (await _rwLock.WriterLockAsync(timeout, ct))
{
// Exclusive access - no other readers or writers
await WriteDataAsync();
}
}
public async Task UpgradeableReadWriteAsync(CancellationToken ct)
{
using (var upgr = await _rwLock.UpgradeableReaderLockAsync(ct))
{
// Multiple readers and one upgradeable reader can hold the lock concurrently
await ReadDataAsync();
// upgrade the reader to perform a write operation
using (await upgr.UpgradeToWriterLockAsync(ct))
{
// Exclusive access - no other readers or writers
await WriteDataAsync();
}
}
}
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
AsyncReaderWriterLock(bool, IGetPooledManualResetValueTaskSource<Releaser>?)
Constructs a new AsyncReaderWriterLock instance.
public AsyncReaderWriterLock(bool runContinuationAsynchronously = true, IGetPooledManualResetValueTaskSource<AsyncReaderWriterLock.Releaser>? pool = null)
Parameters
runContinuationAsynchronouslyboolIndicates if continuations are forced to run asynchronously.
poolIGetPooledManualResetValueTaskSource<AsyncReaderWriterLock.Releaser>Custom pool for waiters.
Properties
CurrentReaderCount
Gets the current number of readers holding the lock. May also include an upgradeable reader if present.
public int CurrentReaderCount { get; }
Property Value
IsReadLockHeld
Gets whether the lock is currently held by any readers.
public bool IsReadLockHeld { get; }
Property Value
IsUpgradeableReadLockHeld
Gets whether the lock is currently held by an upgradeable reader.
public bool IsUpgradeableReadLockHeld { get; }
Property Value
IsUpgradedWriterLockHeld
Gets whether the lock is currently held by an upgraded writer.
public bool IsUpgradedWriterLockHeld { get; }
Property Value
IsWriteLockHeld
Gets whether the lock is currently held by a writer.
public bool IsWriteLockHeld { get; }
Property Value
RunContinuationAsynchronously
Gets or sets whether to force continuations to run asynchronously.
public bool RunContinuationAsynchronously { get; set; }
Property Value
WaitingReaderCount
Gets the number of readers waiting for the lock.
public int WaitingReaderCount { get; }
Property Value
WaitingUpgradeableReaderCount
Gets the number of readers waiting for the lock.
public int WaitingUpgradeableReaderCount { get; }
Property Value
WaitingUpgradedWriterCount
Gets the number of upgraded writers waiting for the lock.
public int WaitingUpgradedWriterCount { get; }
Property Value
WaitingWriterCount
Gets the number of writers waiting for the lock.
public int WaitingWriterCount { get; }
Property Value
Methods
ReaderLockAsync(CancellationToken)
Asynchronously acquires a reader lock.
public ValueTask<AsyncReaderWriterLock.Releaser> ReaderLockAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationTokenThe cancellation token used to cancel the wait.
Returns
- ValueTask<AsyncReaderWriterLock.Releaser>
A ValueTask<TResult> that completes when the reader lock is acquired.
Remarks
Multiple readers can hold the lock concurrently. If a writer is waiting, new readers will be queued behind the writer to prevent writer starvation.
ReaderLockAsync(TimeSpan, CancellationToken)
Asynchronously acquires a reader lock, or throws if it cannot be acquired before the timeout elapses.
public ValueTask<AsyncReaderWriterLock.Releaser> ReaderLockAsync(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
- ValueTask<AsyncReaderWriterLock.Releaser>
A ValueTask<TResult> that completes when the reader lock is acquired.
Remarks
If the lock is immediately available, the method completes synchronously without allocating any cancellation infrastructure. A CancellationTokenSource is allocated only when the lock cannot be acquired immediately and a finite positive timeout is requested; it is disposed automatically when the returned ValueTask<TResult> is awaited.
Exceptions
- ArgumentOutOfRangeException
Thrown when
timeoutis negative and not equal to InfiniteTimeSpan.- TimeoutException
Thrown when the timeout elapses before the lock can be acquired.
- OperationCanceledException
Thrown when
cancellationTokenis cancelled before the lock can be acquired.
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.
UpgradeableReaderLockAsync(CancellationToken)
Asynchronously acquires an upgradeable reader lock.
public ValueTask<AsyncReaderWriterLock.Releaser> UpgradeableReaderLockAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationTokenThe cancellation token used to cancel the wait.
Returns
- ValueTask<AsyncReaderWriterLock.Releaser>
A ValueTask<TResult> that completes when the upgradeable reader lock is acquired.
Remarks
One upgradeable reader can hold the lock, allowing multiple readers to concurrently execute as well. If the upgradeable reader decides to upgrade to a writer lock, it will wait until all other readers have released the lock. If a writer is waiting, new readers will be queued behind the writer to prevent writer starvation.
UpgradeableReaderLockAsync(TimeSpan, CancellationToken)
Asynchronously acquires an upgradeable reader lock, or throws if it cannot be acquired before the timeout elapses.
public ValueTask<AsyncReaderWriterLock.Releaser> UpgradeableReaderLockAsync(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
- ValueTask<AsyncReaderWriterLock.Releaser>
A ValueTask<TResult> that completes when the upgradeable reader lock is acquired.
Remarks
If the lock is immediately available, the method completes synchronously without allocating any cancellation infrastructure. A CancellationTokenSource is allocated only when the lock cannot be acquired immediately and a finite positive timeout is requested; it is disposed automatically when the returned ValueTask<TResult> is awaited.
Exceptions
- ArgumentOutOfRangeException
Thrown when
timeoutis negative and not equal to InfiniteTimeSpan.- TimeoutException
Thrown when the timeout elapses before the lock can be acquired.
- OperationCanceledException
Thrown when
cancellationTokenis cancelled before the lock can be acquired.
WriterLockAsync(CancellationToken)
Asynchronously acquires a writer lock.
public ValueTask<AsyncReaderWriterLock.Releaser> WriterLockAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationTokenThe cancellation token used to cancel the wait.
Returns
- ValueTask<AsyncReaderWriterLock.Releaser>
A ValueTask<TResult> that completes when the writer lock is acquired.
Remarks
Only one writer can hold the lock at a time, and no readers can hold the lock while a writer has it. Writers are prioritized over new readers.
WriterLockAsync(TimeSpan, CancellationToken)
Asynchronously acquires a writer lock, or throws if it cannot be acquired before the timeout elapses.
public ValueTask<AsyncReaderWriterLock.Releaser> WriterLockAsync(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
- ValueTask<AsyncReaderWriterLock.Releaser>
A ValueTask<TResult> that completes when the writer lock is acquired.
Remarks
If the lock is immediately available, the method completes synchronously without allocating any cancellation infrastructure. A CancellationTokenSource is allocated only when the lock cannot be acquired immediately and a finite positive timeout is requested; it is disposed automatically when the returned ValueTask<TResult> is awaited.
Exceptions
- ArgumentOutOfRangeException
Thrown when
timeoutis negative and not equal to InfiniteTimeSpan.- TimeoutException
Thrown when the timeout elapses before the lock can be acquired.
- OperationCanceledException
Thrown when
cancellationTokenis cancelled before the lock can be acquired.