In this lesson, you will learn how to design a high-performance, asynchronous reader-writer lock that avoids task starvation during periods of extreme contention.
The Challenge of Async Contention
In a traditional synchronous RwLock, a thread blocks until the lock is available. In an asynchronous context, we must yield to the executor instead of blocking the thread. However, standard async RwLock implementations often suffer from "writer starvation," where a constant stream of incoming readers prevents a waiting writer from ever acquiring exclusive access.
To optimize for high contention, we need a custom primitive that tracks a queue of waiting tasks and provides a fairness mechanism.
Building a Fair Async RwLock
The core idea is to maintain a state machine that distinguishes between "Active Readers," "Active Writers," and "Waiting Writers." When a writer requests the lock, it enters a Waiting state, which acts as a barrier: new readers are prevented from entering the lock until the writer has finished its turn.
struct State {
readers: usize,
writer_active: bool,
writer_waiting: bool,
}
pub struct AsyncFairRwLock {
state: Mutex<State>,
reader_wakers: Vec<Waker>,
writer_wakers: Vec<Waker>,
}
By using Waker collections, we can implement a "hand-off" pattern. When the last reader drops, instead of waking all tasks indiscriminately, we check if writer_waiting is true. If so, we wake only the prioritized writer, effectively bypassing the thundering herd problem.
Why Custom Primitives Matter
Using generic locks in high-load scenarios often leads to performance plateaus. By building a custom primitive:
- Prioritization: You can implement strict FIFO ordering for writers to ensure system throughput under load.
- Reduced Wake-up Overhead: By maintaining separate wait-queues for readers and writers, you minimize the number of tasks that wake up only to find the lock still held by an incompatible access level.
- Task Pinning: You can align your wake-up logic with specific
Executor-Specific Task Stealingstrategies to ensure that the task granted the lock is the one closest to the CPU cache holding the data.
Key Takeaway: To optimize for extreme contention, build custom primitives that replace blind wake-ups with a state-aware hand-off mechanism, ensuring writers can claim the lock even when a deluge of readers is present.