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. One upgradeable reader at a time can share access with multiple other readers. Once the upgradeable reader is upgraded to writer, it may have to wait until all readers release the lock. An upgradeable reader may release the lock while still upgraded writers are queued for write access.

┌─────────────────────────────────────────────────────────────────────────────┐
│    ------------                                                             │
│    |          | <-----> READERS                                             │
│    |          | <-----> UPGRADEABLE READER + READERS                        │
│    |   IDLE   | <-----> UPGRADEABLE READER -----> UPGRADED WRITER --\       │
│    | NO LOCKS |         ^                                           |       │
│    |          |         |------- DEMOTE TO UPGRADEABLE READER    <--/       │
│    |          | <--------------- DEMOTE TO IDLE WITHOUT READER   <--/       │
│    |          | <-----> WRITER                                              │
│    ------------                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

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);
    }
}

// Upgradeable reader
public async Task UpdateIfNeededAsync(CancellationToken ct)
{
    using (var upgradeable = await _rwLock.UpgradeableReaderLockAsync(ct))
    {
        if (NeedsUpdate())
        {
            using (await upgradeable.UpgradeToWriterLockAsync(ct))
            {
                await SaveDataAsync();
            }
        }
    }
}

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
IsReadLockHeld bool Gets whether one or more readers currently hold the lock.
IsWriteLockHeld bool Gets whether the lock is currently held by a writer.
IsUpgradeableReadLockHeld bool Gets whether the lock is currently held by an upgradeable reader.
IsUpgradedWriterLockHeld bool Gets whether an upgradeable reader is currently upgraded to writer mode.
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.
WaitingUpgradeableReaderCount int Gets the number of upgradeable readers waiting.
WaitingUpgradedWritersCount int Gets the number of upgrade requests waiting for exclusive access.
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.

UpgradeableReaderLockAsync

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

Asynchronously acquires an upgradeable reader lock. One upgradeable reader can coexist with other readers and may later be promoted to a writer lock.

WriterLockAsync

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

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

Releaser

All lock-acquisition methods return a Releaser struct that implements IDisposable and IAsyncDisposable:

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

When the Releaser originated from UpgradeableReaderLockAsync, it also exposes:

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

This upgrades the currently held upgradeable reader to an exclusive writer lock.

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 Readers and Writers: Waiting writers and readers are released in order

Performance

  • O(1) reader acquisition with fast path when no writers are waiting/holding
  • O(1) writer acquisition with fast path when lock is free
  • O(n) reader batch release when writer releases
  • Zero allocations on the fast path (uncontended) and contended path, unless the ObjectPool is exhausted. A custom pool can be provided to satisfy specific needs.

Benchmark Results

The following benchmarks compare AsyncReaderWriterLock against ReaderWriterLockSlim, Nito.AsyncEx.AsyncReaderWriterLock, Proto.Promises.Threading.AsyncReaderWriterLock, Microsoft.VisualStudio.Threading.AsyncReaderWriterLock, and a reference implementation. Not all implementations support every lock mode; the set of compared implementations varies per benchmark.

Reader Lock Benchmark

Measures the performance of acquiring and releasing reader locks with varying numbers of nested acquisitions. At the lowest iteration count (Iterations = 0), the pooled implementation achieves lower latency than Proto.Promises; from Iterations = 1 onward, Proto.Promises achieves lower per-operation latency, reflecting a lower per-lock-call overhead at the cost of a slightly higher fixed invocation overhead. Both operate with zero allocations. Nito.AsyncEx allocates per acquisition. VS.Threading allocates per acquisition and shows substantially higher latency at all iteration counts.

Description Iterations cancellationType Mean Ratio Allocated
ReaderLock · RWLockSlim · System 0 None 6.774 ns 0.40 -
ReaderLock · AsyncRWLock · Pooled 0 None 17.075 ns 1.00 -
ReaderLock · AsyncRWLock · Proto.Promises 0 None 18.400 ns 1.08 -
ReaderLock · AsyncRWLock · RefImpl 0 None 18.917 ns 1.11 -
ReaderLock · AsyncRWLock · Nito.AsyncEx 0 None 40.428 ns 2.37 320 B
ReaderLock · AsyncRWLock · VS.Threading 0 None 224.937 ns 13.17 208 B
ReaderLock · AsyncRWLock · Pooled 0 NotCancelled 16.943 ns 1.00 -
ReaderLock · AsyncRWLock · Proto.Promises 0 NotCancelled 18.193 ns 1.07 -
ReaderLock · AsyncRWLock · Nito.AsyncEx 0 NotCancelled 39.961 ns 2.36 320 B
ReaderLock · AsyncRWLock · VS.Threading 0 NotCancelled 224.973 ns 13.28 208 B
ReaderLock · RWLockSlim · System 1 None 12.461 ns 0.36 -
ReaderLock · AsyncRWLock · Proto.Promises 1 None 28.467 ns 0.82 -
ReaderLock · AsyncRWLock · Pooled 1 None 34.709 ns 1.00 -
ReaderLock · AsyncRWLock · RefImpl 1 None 34.977 ns 1.01 -
ReaderLock · AsyncRWLock · Nito.AsyncEx 1 None 84.985 ns 2.45 640 B
ReaderLock · AsyncRWLock · VS.Threading 1 None 518.755 ns 14.95 416 B
ReaderLock · AsyncRWLock · Proto.Promises 1 NotCancelled 28.772 ns 0.84 -
ReaderLock · AsyncRWLock · Pooled 1 NotCancelled 34.054 ns 1.00 -
ReaderLock · AsyncRWLock · Nito.AsyncEx 1 NotCancelled 81.086 ns 2.38 640 B
ReaderLock · AsyncRWLock · VS.Threading 1 NotCancelled 518.476 ns 15.23 416 B
ReaderLock · RWLockSlim · System 10 None 61.761 ns 0.31 -
ReaderLock · AsyncRWLock · Proto.Promises 10 None 142.806 ns 0.72 -
ReaderLock · AsyncRWLock · RefImpl 10 None 144.870 ns 0.74 -
ReaderLock · AsyncRWLock · Pooled 10 None 197.022 ns 1.00 -
ReaderLock · AsyncRWLock · Nito.AsyncEx 10 None 481.419 ns 2.44 3520 B
ReaderLock · AsyncRWLock · VS.Threading 10 None 3,703.521 ns 18.80 2288 B
ReaderLock · AsyncRWLock · Proto.Promises 10 NotCancelled 147.119 ns 0.74 -
ReaderLock · AsyncRWLock · Pooled 10 NotCancelled 197.696 ns 1.00 -
ReaderLock · AsyncRWLock · Nito.AsyncEx 10 NotCancelled 473.137 ns 2.39 3520 B
ReaderLock · AsyncRWLock · VS.Threading 10 NotCancelled 3,614.872 ns 18.29 2288 B
ReaderLock · RWLockSlim · System 100 None 559.414 ns 0.33 -
ReaderLock · AsyncRWLock · RefImpl 100 None 1,220.891 ns 0.72 -
ReaderLock · AsyncRWLock · Proto.Promises 100 None 1,236.741 ns 0.73 -
ReaderLock · AsyncRWLock · Pooled 100 None 1,700.744 ns 1.00 -
ReaderLock · AsyncRWLock · Nito.AsyncEx 100 None 4,495.417 ns 2.64 32320 B
ReaderLock · AsyncRWLock · VS.Threading 100 None 87,591.125 ns 51.50 21008 B
ReaderLock · AsyncRWLock · Proto.Promises 100 NotCancelled 1,256.133 ns 0.74 -
ReaderLock · AsyncRWLock · Pooled 100 NotCancelled 1,698.319 ns 1.00 -
ReaderLock · AsyncRWLock · Nito.AsyncEx 100 NotCancelled 4,403.054 ns 2.59 32320 B
ReaderLock · AsyncRWLock · VS.Threading 100 NotCancelled 86,857.592 ns 51.14 21008 B

Writer Lock Benchmark

Measures the performance of acquiring and releasing a single writer lock. Proto.Promises achieves lower uncontended latency than the pooled implementation, with both operating at zero allocations. Nito.AsyncEx allocates per acquisition. VS.Threading allocates per acquisition and shows substantially higher latency.

Description Mean Ratio Allocated
WriterLock · RWLockSlim · System 6.905 ns 0.66 -
WriterLock · AsyncRWLock · Proto.Promises 8.379 ns 0.80 -
WriterLock · AsyncRWLock · Pooled 10.460 ns 1.00 -
WriterLock · AsyncRWLock · RefImpl 18.797 ns 1.80 -
WriterLock · AsyncRWLock · Nito.AsyncEx 54.581 ns 5.22 496 B
WriterLock · AsyncRWLock · VS.Threading 1,027.868 ns 98.27 584 B

Upgradeable Reader Lock Benchmark

Measures the performance of acquiring an upgradeable reader lock in combination with varying numbers of additional reader locks. At the lowest iteration count (Iterations = 0), the pooled implementation is marginally faster; Proto.Promises achieves lower per-operation latency as the number of additional reader locks increases. Both operate with zero allocations. VS.Threading allocates per acquisition and shows substantially higher latency across all iteration counts.

Description Iterations cancellationType Mean Ratio Allocated
UpgradeableReaderLock · RWLockSlim · System 0 None 6.855 ns 0.43 -
UpgradeableReaderLock · AsyncRWLock · Pooled 0 None 15.959 ns 1.00 -
UpgradeableReaderLock · AsyncRWLock · Proto.Promises 0 None 20.142 ns 1.26 -
UpgradeableReaderLock · AsyncRWLock · VS.Threading 0 None 1,096.171 ns 68.69 616 B
UpgradeableReaderLock · AsyncRWLock · Pooled 0 NotCancelled 16.321 ns 1.00 -
UpgradeableReaderLock · AsyncRWLock · Proto.Promises 0 NotCancelled 18.987 ns 1.16 -
UpgradeableReaderLock · AsyncRWLock · VS.Threading 0 NotCancelled 1,127.899 ns 69.11 616 B
UpgradeableReaderLock · RWLockSlim · System 1 None 6.745 ns 0.37 -
UpgradeableReaderLock · AsyncRWLock · Pooled 1 None 18.139 ns 1.00 -
UpgradeableReaderLock · AsyncRWLock · Proto.Promises 1 None 19.333 ns 1.07 -
UpgradeableReaderLock · AsyncRWLock · VS.Threading 1 None 1,089.110 ns 60.04 616 B
UpgradeableReaderLock · AsyncRWLock · Proto.Promises 1 NotCancelled 17.789 ns 0.90 -
UpgradeableReaderLock · AsyncRWLock · Pooled 1 NotCancelled 19.749 ns 1.00 -
UpgradeableReaderLock · AsyncRWLock · VS.Threading 1 NotCancelled 1,138.793 ns 57.77 616 B
UpgradeableReaderLock · RWLockSlim · System 2 None 6.822 ns 0.37 -
UpgradeableReaderLock · AsyncRWLock · Proto.Promises 2 None 17.518 ns 0.96 -
UpgradeableReaderLock · AsyncRWLock · Pooled 2 None 18.268 ns 1.00 -
UpgradeableReaderLock · AsyncRWLock · VS.Threading 2 None 1,082.163 ns 59.24 616 B
UpgradeableReaderLock · AsyncRWLock · Proto.Promises 2 NotCancelled 17.317 ns 0.93 -
UpgradeableReaderLock · AsyncRWLock · Pooled 2 NotCancelled 18.599 ns 1.00 -
UpgradeableReaderLock · AsyncRWLock · VS.Threading 2 NotCancelled 1,124.084 ns 60.44 616 B
UpgradeableReaderLock · RWLockSlim · System 5 None 24.061 ns 0.35 -
UpgradeableReaderLock · AsyncRWLock · Proto.Promises 5 None 54.872 ns 0.80 -
UpgradeableReaderLock · AsyncRWLock · Pooled 5 None 68.248 ns 1.00 -
UpgradeableReaderLock · AsyncRWLock · VS.Threading 5 None 2,631.592 ns 38.56 1240 B
UpgradeableReaderLock · AsyncRWLock · Proto.Promises 5 NotCancelled 53.997 ns 0.80 -
UpgradeableReaderLock · AsyncRWLock · Pooled 5 NotCancelled 67.902 ns 1.00 -
UpgradeableReaderLock · AsyncRWLock · VS.Threading 5 NotCancelled 2,693.549 ns 39.67 1240 B

Upgraded Writer Lock Benchmark

Measures the performance of acquiring an upgradeable reader lock, holding additional reader locks concurrently, then upgrading to an exclusive writer lock. The pooled implementation is marginally faster at the lowest iteration count (Iterations = 0); Proto.Promises achieves lower per-operation latency as the number of held reader locks increases. Both operate with zero allocations. VS.Threading allocates proportionally to the number of held reader locks and shows substantially higher latency across all configurations.

Description Iterations cancellationType Mean Ratio Allocated
UpgradedWriterLock · RWLockSlim · System 0 None 13.46 ns 0.60 -
UpgradedWriterLock · AsyncRWLock · Proto.Promises 0 None 21.66 ns 0.97 -
UpgradedWriterLock · AsyncRWLock · Pooled 0 None 22.44 ns 1.00 -
UpgradedWriterLock · AsyncRWLock · VS.Threading 0 None 1,823.37 ns 81.24 824 B
UpgradedWriterLock · AsyncRWLock · Pooled 0 NotCancelled 22.76 ns 1.00 -
UpgradedWriterLock · AsyncRWLock · Proto.Promises 0 NotCancelled 24.61 ns 1.08 -
UpgradedWriterLock · AsyncRWLock · VS.Threading 0 NotCancelled 1,910.35 ns 83.95 824 B
UpgradedWriterLock · RWLockSlim · System 1 None 20.15 ns 0.41 -
UpgradedWriterLock · AsyncRWLock · Proto.Promises 1 None 43.06 ns 0.87 -
UpgradedWriterLock · AsyncRWLock · Pooled 1 None 49.25 ns 1.00 -
UpgradedWriterLock · AsyncRWLock · VS.Threading 1 None 2,325.23 ns 47.21 1032 B
UpgradedWriterLock · AsyncRWLock · Pooled 1 NotCancelled 60.90 ns 1.00 -
UpgradedWriterLock · AsyncRWLock · Proto.Promises 1 NotCancelled 70.48 ns 1.16 -
UpgradedWriterLock · AsyncRWLock · VS.Threading 1 NotCancelled 2,406.80 ns 39.52 1032 B
UpgradedWriterLock · RWLockSlim · System 2 None 25.36 ns 0.37 -
UpgradedWriterLock · AsyncRWLock · Proto.Promises 2 None 54.15 ns 0.80 -
UpgradedWriterLock · AsyncRWLock · Pooled 2 None 68.01 ns 1.00 -
UpgradedWriterLock · AsyncRWLock · VS.Threading 2 None 2,823.31 ns 41.56 1240 B
UpgradedWriterLock · AsyncRWLock · Pooled 2 NotCancelled 76.14 ns 1.00 -
UpgradedWriterLock · AsyncRWLock · Proto.Promises 2 NotCancelled 86.37 ns 1.13 -
UpgradedWriterLock · AsyncRWLock · VS.Threading 2 NotCancelled 2,948.51 ns 38.73 1240 B
UpgradedWriterLock · RWLockSlim · System 5 None 41.38 ns 0.33 -
UpgradedWriterLock · AsyncRWLock · Proto.Promises 5 None 92.94 ns 0.75 -
UpgradedWriterLock · AsyncRWLock · Pooled 5 None 123.96 ns 1.00 -
UpgradedWriterLock · AsyncRWLock · VS.Threading 5 None 4,468.96 ns 36.05 1864 B
UpgradedWriterLock · AsyncRWLock · Proto.Promises 5 NotCancelled 116.85 ns 0.90 -
UpgradedWriterLock · AsyncRWLock · Pooled 5 NotCancelled 130.50 ns 1.00 -
UpgradedWriterLock · AsyncRWLock · VS.Threading 5 NotCancelled 4,578.74 ns 35.09 1864 B

Benchmark Analysis

Key Findings:

  1. Reader and upgradeable reader performance: At a single acquisition per call, the pooled implementation has a slight latency advantage. As the number of lock operations per call increases, Proto.Promises achieves lower per-operation latency in both the reader and upgradeable reader benchmarks, with zero allocations in both cases.

  2. Writer and upgraded writer performance: For single-operation writer lock acquisition, Proto.Promises achieves lower uncontended latency than the pooled implementation. The same pattern holds for the upgraded writer benchmark: the pooled implementation is marginally faster at minimal load, while Proto.Promises achieves lower per-operation latency as the number of additionally held reader locks increases. Both implementations operate with zero allocations.

  3. Writer priority: The writer-priority design prevents writer starvation but may reduce reader throughput when writers are frequently queued.

  4. Memory efficiency: A shared pool for readers, upgradeable readers, and writers allows fine-tuned pool sizing. The pooled implementation maintains zero allocations across all published benchmarks, matching Proto.Promises. Nito.AsyncEx allocates per acquisition in the reader and writer benchmarks. VS.Threading allocates per acquisition in all benchmarks, with memory usage growing proportionally to the number of concurrently held locks.

  5. Releaser struct: The value-type Releaser produces no allocation for the lock handle itself. Allocations occur only for the pooled IValueTaskSource when the pool is exhausted under sustained contention.

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