The idea: reading together, writing alone
Many threads can read the same data at the same time without harm. Only a write needs the data to itself. A readers–writer lock (rwlock) has two ways to take it:
- read lock (
pthread_rwlock_rdlock): shared. Any number of readers may hold it at once, as long as no writer does. - write lock (
pthread_rwlock_wrlock): exclusive. A writer holds it alone: no readers, no other writer.
Both are released with pthread_rwlock_unlock. On the canvas they are shortened to rdlock, wrlock and unlock. With a plain mutex, readers would have to take turns even though they don't change anything.
How to use the animation
Each thread has its own CPU. Readers are R1, R2, R3 and the writer is W. Pick a scenario tab, then Step Tick or Run to End. The box rw shows how many readers hold the lock and which writer holds it. A thread that cannot get the lock goes to sleep in the kernel's wait queue and its CPU turns grey (free). The strip at the bottom shows every CPU's ticks: green C = reading or writing, Z = asleep.
Readers share
In Readers share three readers each read for 4 ticks. Their reads overlap, and at tick 5 all three are inside together. Everything is done by tick 9. With a mutex the 12 ticks of reading would have run one after another.
A writer waits for the readers
A writer must wait until the last reader leaves. In Writer waits for readers W asks at tick 3 while R1 is reading, sleeps, and is woken by R1's unlock at tick 7, which hands it the lock. While W writes, R2 arrives and has to sleep in turn. W's unlock wakes R2.
Writer starvation
Which waiting thread gets the lock is decided by the lock's policy:
| Policy | A new reader while a writer waits | Risk |
|---|---|---|
prefer readers (glibc default, PTHREAD_RWLOCK_PREFER_READER_NP) | goes in, if other readers are inside | writers starve |
prefer writers (PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP) | sleeps behind the writer | readers wait longer |
In Writer starves three readers read twice each, and their reads overlap. Every time one reader leaves, another one is already inside, so the number of readers never drops to 0 until tick 22. W asks at tick 3 and waits 20 ticks for a 3-tick write. With more readers it could wait forever.
Prefer writers runs the same threads. Now R2 and R3 sleep behind the waiting writer, R1 finishes, and W gets the lock after 5 ticks. The price: the readers wait more, and the whole run ends at tick 29 instead of 27. Fair locks such as Java's ReentrantReadWriteLock(true) serve threads in arrival order.
The page Readers–Writers Problem builds these same policies from semaphores, one atomic line at a time.
When is an rwlock worth it?
- Reads are much more frequent than writes, and
- each read holds the lock long enough that readers really overlap.
For very short reads an rwlock can be slower than a mutex: every reader still writes to the shared reader count, so the cache line bounces between CPUs. Read-mostly data in the Linux kernel often uses RCU or seqlocks instead, where readers write nothing at all.
Where you meet it
- C:
pthread_rwlock_t; C++:std::shared_mutexwithlock_shared(). - Java:
ReentrantReadWriteLock, andStampedLockwith optimistic reads. - Linux kernel:
rw_semaphore(sleeps) andrwlock_t(spins). - Databases: shared (S) and exclusive (X) row and table locks follow the same rule; see Optimistic vs Pessimistic Locking.
What the page leaves out
- Upgrading a read lock to a write lock (two readers trying to upgrade deadlock each other) and downgrading a write lock to a read lock.
- How glibc stores the state: one word with the reader count and flag bits, and futexes for sleeping, like the mutex. Here the lock is handed over on wake-up to keep the picture simple.
- A reader that takes the read lock again while a writer waits deadlocks under prefer writers (hence "NONRECURSIVE").
- Real timing: a system call and a context switch cost far more than one tick.