Table of Contents

AsyncReaderWriterLock

A pooled, allocation-free async reader-writer lock that supports multiple concurrent readers or a single exclusive writer using ValueTask-based waiters with cancellation tokens.

Overview

AsyncReaderWriterLock is an async-compatible reader-writer lock. It allows multiple readers to enter the lock concurrently, but only one writer can hold the lock exclusively. Writers are prioritized over readers to prevent writer starvation.

Usage

Basic Usage

using CryptoHives.Foundation.Threading.Async.Pooled;

private readonly AsyncReaderWriterLock _rwLock = new AsyncReaderWriterLock();

// Reader
public async Task<Data> ReadDataAsync(CancellationToken ct)
{
    using (await _rwLock.ReaderLockAsync(ct))
    {
        // Multiple readers can hold the lock concurrently
        return await FetchDataAsync();
    }
}

// Writer
public async Task WriteDataAsync(Data data, CancellationToken ct)
{
    using (await _rwLock.WriterLockAsync(ct))
    {
        // Exclusive access - no other readers or writers
        await SaveDataAsync(data);
    }
}

Cache Pattern

private readonly AsyncReaderWriterLock _cacheLock = new AsyncReaderWriterLock();
private Dictionary<string, object> _cache = new();

public async Task<T> GetOrAddAsync<T>(string key, Func<Task<T>> factory, CancellationToken ct)
{
    // Try to read first
    using (await _cacheLock.ReaderLockAsync(ct))
    {
        if (_cache.TryGetValue(key, out var cached))
        {
            return (T)cached;
        }
    }

    // Need to write
    using (await _cacheLock.WriterLockAsync(ct))
    {
        // Double-check after acquiring write lock
        if (_cache.TryGetValue(key, out var cached))
        {
            return (T)cached;
        }

        var value = await factory();
        _cache[key] = value;
        return value;
    }
}

Constructor

public AsyncReaderWriterLock(
    bool runContinuationAsynchronously = true,
    IGetPooledManualResetValueTaskSource<Releaser>? pool = null)
Parameter Description
runContinuationAsynchronously If true (default), continuations run on the thread pool.
pool Optional custom pool for ValueTaskSource instances used by both readers and writers.

Properties

Property Type Description
IsReaderLockHeld bool Gets whether any readers hold the lock.
IsWriterLockHeld bool Gets whether a writer holds the lock.
CurrentReaderCount int Gets the number of readers holding the lock.
WaitingWriterCount int Gets the number of writers waiting.
WaitingReaderCount int Gets the number of readers waiting.
RunContinuationAsynchronously bool Gets or sets whether continuations run asynchronously.

Methods

ReaderLockAsync

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

Asynchronously acquires a reader lock. Multiple readers can hold the lock concurrently.

WriterLockAsync

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

Asynchronously acquires a writer lock. Only one writer can hold the lock.

Releaser

Both methods return a Releaser struct that implements IDisposable and IAsyncDisposable:

using (await _rwLock.ReaderLockAsync())
{
    // Lock is held here
}
// Lock is automatically released

Fairness and Priority

  • Writer Priority: New readers are queued behind waiting writers to prevent writer starvation
  • Reader Batching: When a writer releases, all waiting readers are released together
  • FIFO Writers: Waiting writers are released in order

Performance

  • O(1) reader acquisition when no writers are waiting/holding
  • O(1) writer acquisition when lock is free
  • O(n) reader batch release when writer releases
  • Zero allocations on the fast path (uncontended)

Benchmark Results

The following benchmarks compare AsyncReaderWriterLock against ReaderWriterLockSlim, Nito.AsyncEx.AsyncReaderWriterLock and a reference implementation. TODO: Currently benchmarks are only available on uncontended scenarios to measure the overhead of a single lock acquisition and release.

Reader Lock Benchmark

Measures the performance of acquiring and releasing reader locks.

Description Mean Ratio Allocated
ReaderLock · ReaderWriterLockSlim · RWLockSlim 5.870 ns 0.15 -
ReaderLock · AsyncRWLock · RefImpl 15.499 ns 0.38 -
ReaderLock · AsyncRWLock · Nito.AsyncEx 36.934 ns 0.91 320 B
ReaderLock · AsyncRWLock · Pooled 40.418 ns 1.00 -

Writer Lock Benchmark

Measures the performance of acquiring and releasing writer locks.

Description Mean Ratio Allocated
WriterLock · ReaderWriterLockSlim · RWLockSlim 5.767 ns 0.35 -
WriterLock · AsyncRWLock · Pooled 16.449 ns 1.00 -
WriterLock · AsyncRWLock · RefImpl 18.603 ns 1.13 -
WriterLock · AsyncRWLock · Nito.AsyncEx 51.367 ns 3.12 496 B

Benchmark Analysis

Key Findings:

  1. Reader Performance: Uncontended reader lock acquisition is extremely fast with zero allocations. Multiple concurrent readers can proceed without blocking each other.

  2. Writer Priority: The writer-priority design prevents starvation but may impact reader throughput when writers are frequently waiting.

  3. Memory Efficiency: One pool for readers and writers allow fine-tuned pool sizing based on workload characteristics.

  4. Releaser Struct: The value-type Releaser ensures no allocation for the lock handle itself, only for the IValueTaskSource when contention occurs.

When to Choose AsyncReaderWriterLock:

  • Read-heavy workloads with occasional writes
  • Cache implementations with read/write patterns
  • Document or configuration stores

Design Trade-offs:

  • Writer priority may reduce reader throughput under write-heavy loads
  • Consider AsyncLock for simpler mutex-style locking
  • For read-only scenarios, no lock is needed

Comparison with ReaderWriterLockSlim

Feature AsyncReaderWriterLock ReaderWriterLockSlim
Async support Native None
Allocation overhead Minimal (pooled) None (sync)
Writer priority Yes Configurable
Cancellation Full support None

See Also


© 2026 The Keepers of the CryptoHives