Table of Contents

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.

Considering this class to be a state machine, the states are:
------------
|          | <-----> 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

runContinuationAsynchronously bool

Indicates if continuations are forced to run asynchronously.

pool IGetPooledManualResetValueTaskSource<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

int

IsReadLockHeld

Gets whether the lock is currently held by any readers.

public bool IsReadLockHeld { get; }

Property Value

bool

IsUpgradeableReadLockHeld

Gets whether the lock is currently held by an upgradeable reader.

public bool IsUpgradeableReadLockHeld { get; }

Property Value

bool

IsUpgradedWriterLockHeld

Gets whether the lock is currently held by an upgraded writer.

public bool IsUpgradedWriterLockHeld { get; }

Property Value

bool

IsWriteLockHeld

Gets whether the lock is currently held by a writer.

public bool IsWriteLockHeld { get; }

Property Value

bool

RunContinuationAsynchronously

Gets or sets whether to force continuations to run asynchronously.

public bool RunContinuationAsynchronously { get; set; }

Property Value

bool

WaitingReaderCount

Gets the number of readers waiting for the lock.

public int WaitingReaderCount { get; }

Property Value

int

WaitingUpgradeableReaderCount

Gets the number of readers waiting for the lock.

public int WaitingUpgradeableReaderCount { get; }

Property Value

int

WaitingUpgradedWriterCount

Gets the number of upgraded writers waiting for the lock.

public int WaitingUpgradedWriterCount { get; }

Property Value

int

WaitingWriterCount

Gets the number of writers waiting for the lock.

public int WaitingWriterCount { get; }

Property Value

int

Methods

ReaderLockAsync(CancellationToken)

Asynchronously acquires a reader lock.

public ValueTask<AsyncReaderWriterLock.Releaser> ReaderLockAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

The 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

timeout TimeSpan

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

cancellationToken CancellationToken

The 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 timeout is negative and not equal to InfiniteTimeSpan.

TimeoutException

Thrown when the timeout elapses before the lock can be acquired.

OperationCanceledException

Thrown when cancellationToken is 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

bool

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

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

cancellationToken CancellationToken

The 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

timeout TimeSpan

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

cancellationToken CancellationToken

The 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 timeout is negative and not equal to InfiniteTimeSpan.

TimeoutException

Thrown when the timeout elapses before the lock can be acquired.

OperationCanceledException

Thrown when cancellationToken is cancelled before the lock can be acquired.

WriterLockAsync(CancellationToken)

Asynchronously acquires a writer lock.

public ValueTask<AsyncReaderWriterLock.Releaser> WriterLockAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

The 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

timeout TimeSpan

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

cancellationToken CancellationToken

The 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 timeout is negative and not equal to InfiniteTimeSpan.

TimeoutException

Thrown when the timeout elapses before the lock can be acquired.

OperationCanceledException

Thrown when cancellationToken is cancelled before the lock can be acquired.