156. RwLock<T> — Many Readers OR One Writer, When Reads Dominate
This morning’s Mutex<T> treats every caller the same: one at a time, no matter what they’re doing. If ninety-nine of them only want to read, that’s ninety-nine threads serialized behind a lock they didn’t need. RwLock<T> splits the door in two — many readers OR one writer — so read-heavy workloads actually fan out.
The pain: Mutex serializes readers too
A Mutex doesn’t know or care whether you’re going to mutate. Two threads that just want to peek at a config still queue up:
| |
Eight threads, eight reads, zero writes — and they still run one at a time. For a config that’s read on every request and updated once an hour, that’s a lot of wasted parallelism.
The fix: read() and write()
RwLock<T> has two acquire methods, and they map directly onto the two halves of the aliasing rule:
| |
read() hands back a RwLockReadGuard that derefs to &T. write() hands back a RwLockWriteGuard that derefs to &mut T. Both release on drop. The whole point: any number of read() guards can be alive at once, as long as no write() guard is.
The classic shape: Arc<RwLock<T>> with many readers
The pattern is the same Arc<_>-wraps-the-shared-thing shape as Arc<Mutex<T>>, but the parallelism story changes. Readers actually run at the same time:
| |
The point isn’t the assertion — it’s that the eight readers can interleave freely on real hardware. Swap in a Mutex and they’d be a stairstep.
The footgun: writer starvation
A reader-heavy workload can keep the lock in “shared” mode forever. A writer waiting on write() blocks every new reader from joining (on most platforms), but the current readers keep working — and as soon as one of them is done, if a new reader sneaks in before the writer is scheduled, the writer stays parked. The standard library’s RwLock does not promise any particular fairness policy, and historically the behavior varied per OS.
Two practical takeaways:
- Keep the write path short. Compute what you want to write outside the lock; take
write()only to swap the result in. - If you find yourself reaching for “give readers priority” or “give writers priority” knobs, you’ve outgrown
std::sync::RwLock. Either restructure to publish snapshots throughArc::newswaps, or pull inparking_lot::RwLockwhich exposes fairness controls.
The other footgun: holding the read guard across a write
Same shape as the Mutex “hold the guard too long” bug, but uglier — because nested re-entry on the same thread will deadlock, not panic:
| |
RefCell would panic loudly with already borrowed. RwLock will silently park the thread, and you’ll see it only in a stack dump. When in doubt, drop the guard explicitly before any call you don’t control.
Going the other way: downgrade to keep reading what you just wrote
Once you’ve finished a write and want to keep reading the same value without a release/reacquire window, RwLockWriteGuard::downgrade is the atomic way across. Worth knowing about for the cache-refresh shape, where the writer thread immediately turns into a long-lived reader.
When to pick RwLock vs Mutex
Mutex<T> for short critical sections, mixed read/write workloads, or anywhere the per-operation cost of a lock matters. Cheaper per acquire, simpler mental model, no fairness surprises.
RwLock<T> when reads vastly outnumber writes and the read critical section is non-trivial — long enough that running them in parallel actually pays for the higher per-acquire cost. Read-only config lookups served on every request, periodically refreshed snapshots, anything shaped like “1000 readers, 1 writer per minute.”
If reads are short (a single field load) and contention is low, plain Mutex is often faster in practice — the extra bookkeeping in RwLock isn’t free. Measure before assuming the read-write split is a win.