Reader-Writer Locks in Process Synchronization
Most synchronization problems treat all processes the same: everyone competes for exclusive access and takes turns one at a time. But in reality, not all access to shared data is equal.
Reading something does not change it, so two readers going at the same time is completely harmless. Writing is a different story. A writer modifying shared data while someone else is reading it, or while another writer is also modifying it, can cause serious problems. The reader-writer problem is about handling this distinction properly.
The Core Rules
You have a shared resource, say a database or a file. Some processes only read from it. Others need to write to it. The rules are:
- Multiple readers can access the data at the same time without any issue since they are not changing anything.
- A writer needs exclusive access. While a writer is working, nobody else, whether reader or another writer, should be allowed in.
- If a writer is in the middle of an update and a reader reads at the same time, the reader might get half-old and half-new data, which is inconsistent and potentially wrong.
- If two writers go at the same time, their changes can conflict and corrupt the data entirely.
Allowed combinations:
Reader + Reader -> SAFE (no data changes)
Reader + Writer -> UNSAFE (inconsistent read)
Writer + Writer -> UNSAFE (data corruption)
Writer alone -> SAFE (exclusive access)
Access Matrix:
Reader Writer
Reader OK BLOCK
Writer BLOCK BLOCKTwo Approaches to Priority
| Approach | How It Works | Advantage | Risk |
|---|---|---|---|
| Reader Preference | As long as at least one reader is active, writers wait. New readers can jump in even if a writer is already waiting. | Great for read-heavy systems. Maximum read parallelism. | Writers can starve if readers keep arriving continuously. |
| Writer Preference | Once a writer is ready, it gets priority. Readers wait until the writer is done. | Writers are never stuck behind an endless stream of readers. | Can slow down reading throughput. Readers may experience delays. |
Solving It With Semaphores
The classic semaphore-based solution (reader preference) uses two semaphores and one integer variable:
- write_sem (initialized to 1): Controls access to the shared resource for writing. Both readers and writers use it, but in different ways.
- mutex_sem (initialized to 1): Protects the readcount variable so that only one reader updates it at a time.
- readcount (integer, starts at 0): Tracks how many readers are currently active.
Writer Logic
A writer simply waits on the write semaphore. If it is free, the writer gets in and does its work. Nobody else can enter while the write semaphore is held. When done, the writer signals it.
Writer:
write_sem.wait() // Request exclusive access
// --- Write to shared resource ---
write_sem.signal() // Release accessReader Logic
Readers are more involved. The first reader to arrive locks out writers. The last reader to leave lets writers back in. In between, any number of readers can enter freely.
Reader (Entry):
mutex_sem.wait() // Protect readcount
readcount++
if (readcount == 1) // First reader?
write_sem.wait() // Block writers
mutex_sem.signal() // Let other readers update readcount
// --- Read from shared resource ---
Reader (Exit):
mutex_sem.wait() // Protect readcount
readcount--
if (readcount == 0) // Last reader?
write_sem.signal() // Let writers in
mutex_sem.signal()Implementation in C++
#include <iostream>
#include <thread>
#include <mutex>
#include <semaphore>
#include <chrono>
#include <vector>
std::counting_semaphore<1> write_sem(1); // Controls write access
std::counting_semaphore<1> mutex_sem(1); // Protects readcount
int readcount = 0; // Active readers
int shared_data = 0; // The shared resource
void writer(int id) {
for (int i = 0; i < 3; i++) {
std::cout << "Writer " << id << " waiting to write..." << std::endl;
write_sem.acquire(); // Exclusive access
shared_data++;
std::cout << "Writer " << id << " writing. Data is now: "
<< shared_data << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "Writer " << id << " done writing." << std::endl;
write_sem.release(); // Release access
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
}
void reader(int id) {
for (int i = 0; i < 3; i++) {
std::cout << "Reader " << id << " waiting to read..." << std::endl;
// Entry: update readcount safely
mutex_sem.acquire();
readcount++;
if (readcount == 1)
write_sem.acquire(); // First reader blocks writers
mutex_sem.release();
// Read the shared resource
std::cout << "Reader " << id << " reading. Data is: "
<< shared_data << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(300));
std::cout << "Reader " << id << " done reading." << std::endl;
// Exit: update readcount safely
mutex_sem.acquire();
readcount--;
if (readcount == 0)
write_sem.release(); // Last reader unblocks writers
mutex_sem.release();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
}
int main() {
std::cout << "Reader-Writer Problem Simulation" << std::endl;
std::cout << "Initial shared data: " << shared_data << std::endl;
std::cout << "-----------------------------------" << std::endl;
std::thread w1(writer, 1);
std::thread r1(reader, 1);
std::thread r2(reader, 2);
std::thread r3(reader, 3);
w1.join(); r1.join(); r2.join(); r3.join();
std::cout << "-----------------------------------" << std::endl;
std::cout << "Final shared data: " << shared_data << std::endl;
return 0;
}Output:
Reader-Writer Problem Simulation
Initial shared data: 0
-----------------------------------
Writer 1 waiting to write...
Reader 1 waiting to read...
Reader 2 waiting to read...
Reader 3 waiting to read...
Writer 1 writing. Data is now: 1
Writer 1 done writing.
Reader 1 reading. Data is: 1
Reader 2 reading. Data is: 1
Reader 3 reading. Data is: 1
Reader 1 done reading.
Reader 2 done reading.
Reader 3 done reading.
Writer 1 writing. Data is now: 2
...
-----------------------------------
Final shared data: 3Multiple readers access the data at the same time without blocking each other. The writer waits until all readers are done, then gets exclusive access. Once writing is complete, readers can come back in.
Where This Shows Up in Real Systems
| System | Readers | Writers | Why RW Locks Help |
|---|---|---|---|
| Databases | SELECT queries from many users | UPDATE/DELETE operations | Parallel reads massively improve query throughput |
| File Systems | Multiple programs opening a file for reading | A program writing or appending to the file | Prevents reading half-written data |
| Server Config | Hundreds of threads reading the config | Admin pushing a config update | No thread reads a half-updated configuration |
| DNS Caches | Thousands of lookups per second | Periodic cache refresh from upstream | Lookups continue in parallel during normal operation |
| In-Memory Caches | Application threads reading cached data | Background thread refreshing stale entries | Read-heavy workloads get near-zero contention |
Reader-Writer Lock vs Plain Mutex
A plain mutex would force readers to take turns even though they could safely go at the same time. The reader-writer approach lets you get real parallelism out of read-heavy workloads while still protecting writes properly. For systems where reads vastly outnumber writes, the performance difference can be dramatic.
Reader-Writer Scenario
Question 1 of 1Test your understanding of concurrent reader-writer access.
