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.

Two panels. rdlock: readers R1, R2 and R3 all reach the shared data at once while the writer W sleeps in the wait queue. wrlock: W alone reaches the shared data while R1, R2 and R3 sleep.
Readers share the lock with each other; a writer holds it alone.

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:

PolicyA new reader while a writer waitsRisk
prefer readers (glibc default, PTHREAD_RWLOCK_PREFER_READER_NP)goes in, if other readers are insidewriters starve
prefer writers (PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP)sleeps behind the writerreaders 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.

Two schematic timelines with the same threads. Prefer readers: the reads of R1, R2 and R3 overlap so a reader is always inside, and W, asking at tick 3, waits 20 ticks before its write. Prefer writers: R2 and R3 sleep behind the waiting writer, R1 finishes, W writes after waiting 5 ticks, then R2 and R3 read.
Under prefer readers, overlapping reads keep the writer out for 20 ticks; prefer writers makes new readers wait so W gets in after 5.

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_mutex with lock_shared().
  • Java: ReentrantReadWriteLock, and StampedLock with optimistic reads.
  • Linux kernel: rw_semaphore (sleeps) and rwlock_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.