Readers-Writers Problem
Writers Preference
Read aloud is unavailable in this browser.
Guaranteeing Writer Access
The Readers Preference solution's Achilles' heel is writer starvation. In systems where writes carry time-sensitive information—stock prices, security patches, configuration updates—indefinite writer delays are unacceptable. The Writers Preference solution, also known as the Second Readers-Writers Problem, inverts the priority: writers take precedence over waiting readers.
This approach fundamentally changes the synchronization dynamics. When a writer is waiting, no new readers may start. Readers currently in the critical section finish, but all newly arriving readers must wait behind the writer. This guarantees that writers eventually access the resource, but at the cost of potentially starving readers under write-heavy workloads.
By the end of this page, you will understand: why writer priority requires additional synchronization primitives; the complete implementation with four semaphores; how the 'writer waiting' signal blocks new readers; proof of correctness including starvation properties; performance implications of writer priority; and scenarios where writer preference is the right choice.
Motivation for Writer Priority
The need for writer priority arises in scenarios where write delays have consequences beyond simple latency.
Scenario 1: Security Updates
Consider a security system where a critical patch must be applied to a shared policy table. Until the patch is applied, the system remains vulnerable. If readers (policy checks) continuously arrive, the security update starves—the vulnerability persists indefinitely. Writer priority ensures security updates complete promptly.
Scenario 2: Real-Time Data Feeds
A stock trading system receives price updates (writes) and serves price queries (reads). Stale prices can lead to incorrect trades. If a price update waits behind thousands of queries, trades execute on outdated information. Losses accumulate. Writer priority ensures price updates propagate immediately.
Scenario 3: Consistency Requirements
Some systems require writes to complete before related reads can proceed. If a write creates a new resource, subsequent reads must see it. Writer starvation breaks this semantic—reads keep returning 'not found' because the creating write never executes.
| Domain | Write Operations | Consequence of Delay |
|---|---|---|
| Security | Policy updates, revocations | Extended vulnerability window |
| Finance | Price updates, trades | Stale data causes losses |
| Configuration | Feature flags, settings | Inconsistent behavior |
| Inventory | Stock updates | Overselling, stockouts |
| Messaging | New messages | Delayed communication |
Writer priority doesn't eliminate starvation—it redirects it. While writers are protected, readers can now starve under continuous write load. The solution is appropriate when write delays are more costly than read delays, not when starvation should be eliminated entirely.
Design Challenges
Implementing writer priority is more complex than reader priority. The challenge is allowing current readers to finish while blocking new readers when a writer waits.
The Core Question:
How do we communicate to arriving readers that a writer is waiting, so they should block even though other readers are still in the critical section?
Attempted Solution: Simple Flag
A naive approach adds a writer_waiting flag:
12345678910111213141516171819202122232425
// BROKEN: Naive writer priority attemptbool writer_waiting = false; // Flag to indicate waiting writer void reader() { if (writer_waiting) // Check if writer is waiting wait(???); // Problem: what do we wait on? wait(mutex); read_count++; if (read_count == 1) wait(rw_mutex); signal(mutex); read_data(); // ... exit section} void writer() { writer_waiting = true; // Signal to readers wait(rw_mutex); // Wait for exclusive access writer_waiting = false; // No longer waiting write_data(); signal(rw_mutex);}Why This Fails:
- Race condition on flag: Multiple readers might check
writer_waitingsimultaneously with a writer setting it - What do blocked readers wait on? We need a synchronization primitive, not just a flag
- How do readers know when to proceed? After the writer finishes, blocked readers must be woken
- Multiple writers: If multiple writers wait, we need ordered wakeup
Required Additional State:
The Writers Preference solution needs:
- A count of waiting/active writers (not just readers)
- A mechanism to block new readers when writers wait
- Coordination between writers and the reader-blocking mechanism
This requires additional semaphores beyond those in the Readers Preference solution.
The Writers Preference solution requires 3-4 semaphores (vs. 2 for Readers Preference) and careful coordination. This additional complexity is the price of preventing writer starvation. Always consider whether your system actually needs writer priority before implementing it.
Complete Solution Implementation
The Writers Preference solution uses four synchronization primitives. Let's develop it systematically with detailed annotations.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
/* * Second Readers-Writers Problem: Writers Preference Solution * * Invariants: * - Multiple readers can access the resource simultaneously (if no writers waiting) * - Writers have exclusive access * - Once a writer is waiting, no new readers start * - Writers don't starve * * Shared Variables: * - read_count: Number of readers currently in critical section * - write_count: Number of writers currently waiting or writing * - mutex: Protects read_count * - write_mutex: Protects write_count * - rw_mutex: Mutual exclusion between readers and writers * - read_try: Controls reader access (blocked when writer waiting) */ #include <semaphore.h> // Reader trackingint read_count = 0; // Active readerssemaphore mutex = 1; // Protects read_count // Writer trackingint write_count = 0; // Waiting + active writerssemaphore write_mutex = 1; // Protects write_count // Access controlsemaphore rw_mutex = 1; // Resource access controlsemaphore read_try = 1; // Reader entry gate /* * READER PROTOCOL (Writers Preference) * * Entry: Must pass through read_try gate (blocked if writers waiting) * Exit: Last reader releases rw_mutex */void reader(int id) { while (true) { // ========== ENTRY SECTION ========== /* * Step 1: Acquire read_try * This gate is held by waiting writers. * If a writer is waiting, readers block here. */ wait(read_try); // Gate: may block if writer waiting wait(mutex); // Step 2: Lock read_count read_count++; // Step 3: Increment reader count if (read_count == 1) { // Step 4: First reader? wait(rw_mutex); // Step 5: Block writers from resource } signal(mutex); // Step 6: Unlock read_count signal(read_try); // Step 7: Release gate for other readers // ========== CRITICAL SECTION ========== read_data(id); // Actually read the resource // ========== EXIT SECTION ========== wait(mutex); // Step 8: Lock read_count read_count--; // Step 9: Decrement reader count if (read_count == 0) { // Step 10: Last reader? signal(rw_mutex); // Step 11: Allow writers to proceed } signal(mutex); // Step 12: Unlock read_count // ========== REMAINDER SECTION ========== process_data(id); }} /* * WRITER PROTOCOL (Writers Preference) * * Entry: First waiting writer acquires read_try to block new readers * Exit: Last writer releases read_try */void writer(int id) { while (true) { // ========== ENTRY SECTION ========== wait(write_mutex); // Step 1: Lock write_count write_count++; // Step 2: Register as waiting writer if (write_count == 1) { // Step 3: First writer? wait(read_try); // Step 4: Block new readers } signal(write_mutex); // Step 5: Unlock write_count wait(rw_mutex); // Step 6: Wait for exclusive access // ========== CRITICAL SECTION ========== write_data(id); // Exclusively modify resource // ========== EXIT SECTION ========== signal(rw_mutex); // Step 7: Release exclusive access wait(write_mutex); // Step 8: Lock write_count write_count--; // Step 9: Deregister writer if (write_count == 0) { // Step 10: Last writer? signal(read_try); // Step 11: Allow readers again } signal(write_mutex); // Step 12: Unlock write_count // ========== REMAINDER SECTION ========== prepare_next_write(id); }}Understanding the Synchronization Primitives:
| Semaphore | Purpose | Initial Value |
|---|---|---|
mutex | Protects read_count | 1 |
write_mutex | Protects write_count | 1 |
rw_mutex | Mutual exclusion for resource | 1 |
read_try | Gate controlling reader entry | 1 |
The Key Insight: read_try as a Gate
The read_try semaphore is the crucial addition. When no writers are waiting, read_try == 1 and readers pass through immediately. When the first writer arrives, it acquires read_try, setting it to 0. All subsequent readers block at wait(read_try) until all writers finish.
This creates a "velvet rope" effect: readers currently inside can finish, but new readers must wait behind the writer.
Execution Trace Analysis
Let's trace through execution scenarios to understand how writer priority works.
Scenario 1: Writer Arrives While Reader Active
Initial: read_count=0, write_count=0, all semaphores=1 Time 0: Reader R1 enters R1: wait(read_try) ✓ [read_try=0] R1: wait(mutex) ✓ [mutex=0] R1: read_count++ → 1 R1: read_count==1: wait(rw_mutex) ✓ [rw_mutex=0] R1: signal(mutex) [mutex=1] R1: signal(read_try) [read_try=1] R1: ** READING ** Time 1: Writer W1 arrives (while R1 reading) W1: wait(write_mutex) ✓ [write_mutex=0] W1: write_count++ → 1 W1: write_count==1: wait(read_try) ✓ [read_try=0] ← Blocks new readers! W1: signal(write_mutex) [write_mutex=1] W1: wait(rw_mutex) BLOCKS [rw_mutex=0, held by R1] Time 2: Reader R2 arrives (while R1 reading, W1 waiting) R2: wait(read_try) BLOCKS [read_try=0] ← R2 cannot start! Time 3: R1 finishes reading R1: wait(mutex) ✓ R1: read_count-- → 0 R1: read_count==0: signal(rw_mutex) [rw_mutex=1] R1: signal(mutex) Time 3 (continued): W1 unblocks W1: wait(rw_mutex) ✓ [rw_mutex=0] W1: ** WRITING ** Time 4: W1 finishes W1: signal(rw_mutex) [rw_mutex=1] W1: wait(write_mutex) ✓ W1: write_count-- → 0 W1: write_count==0: signal(read_try) [read_try=1] ← Readers unblock! W1: signal(write_mutex) Time 4 (continued): R2 unblocks R2: wait(read_try) ✓ [read_try=0] R2: proceeds to read...Key Observation: R2 arrived before W1 finished, but R2 was blocked. W1 proceeded before R2 even though R2 was waiting. The writer jumped ahead of the reader. This is writer priority in action.
Scenario 2: Multiple Writers Queue
Assume R1 is reading, W1 waiting on rw_mutex, readers blocked on read_try Time 5: Writer W2 arrives W2: wait(write_mutex) ✓ W2: write_count++ → 2 W2: write_count==1? NO (skip wait(read_try)) ← Read_try already held W2: signal(write_mutex) W2: wait(rw_mutex) BLOCKS ← Behind W1 Time 6: R1 finishes, W1 proceeds W1: signal(rw_mutex) [rw_mutex=1] W1: wait(rw_mutex) ✓ [rw_mutex=0] ← W1 now writing Time 7: W1 finishes W1: signal(rw_mutex) [rw_mutex=1] W1: wait(write_mutex) W1: write_count-- → 1 W1: write_count==0? NO (don't signal read_try) ← W2 still waiting! W1: signal(write_mutex) Time 7 (continued): W2 acquires rw_mutex W2: wait(rw_mutex) ✓ [rw_mutex=0] W2: ** WRITING ** Time 8: W2 finishes W2: signal(rw_mutex) W2: wait(write_mutex) W2: write_count-- → 0 W2: write_count==0? YES: signal(read_try) ← NOW readers can proceed W2: signal(write_mutex)In scenario 2, readers remained blocked while two writers executed consecutively. If writers continuously arrive, read_try never gets signaled, and readers starve. This is the symmetric starvation problem to the Readers Preference solution.
Proof of Correctness
We now prove that the Writers Preference solution satisfies the required safety properties.
Theorem 1: Reader-Writer Mutual Exclusion Statement: Readers and writers are never in the critical section simultaneously.
Proof:
- Writers hold
rw_mutexduring their critical section - The first reader acquires
rw_mutexbefore entering - If a writer holds
rw_mutex, the first reader blocks atwait(rw_mutex) - If readers hold
rw_mutex, writers block atwait(rw_mutex) - ∴
rw_mutexenforces mutual exclusion between readers and writers ∎
Theorem 2: Writer-Writer Mutual Exclusion Statement: At most one writer is in the critical section at a time.
Proof:
- Each writer must acquire
rw_mutexbefore entering critical section rw_mutexis a binary semaphore- Only one thread can hold a binary semaphore
- ∴ Only one writer can be in the critical section ∎
Theorem 3: Reader Concurrency Statement: Multiple readers can be in the critical section simultaneously.
Proof:
- Only the first reader acquires
rw_mutex - Subsequent readers check
read_count > 1and skipwait(rw_mutex) - The
read_trysemaphore is acquired and immediately released by each reader - Multiple readers can pass through
read_trysequentially and all enter the critical section mutexonly serializes access toread_count, not to the resource- ∴ Multiple readers coexist in the critical section ∎
Theorem 4: Writer Priority (No Writer Starvation) Statement: A waiting writer eventually enters the critical section.
Proof:
- When a writer calls
wait(write_mutex); write_count++, it's registered as waiting - The first waiting writer acquires
read_try - With
read_tryheld, no new readers can start (they block onwait(read_try)) - Active readers eventually finish (assuming no infinite loops in read)
- The last active reader signals
rw_mutex - The waiting writer acquires
rw_mutexand enters - ∴ Writers don't starve (assuming bounded reader critical sections) ∎
The 'no writer starvation' proof requires that readers eventually exit the critical section. If a reader can hold the resource indefinitely (deadlock, infinite loop, or blocking call), the writer waiting on rw_mutex might wait forever. This is a general requirement, not specific to this solution.
Reader Starvation Analysis
While the Writers Preference solution prevents writer starvation, it introduces the symmetric problem: reader starvation.
The Starvation Mechanism:
Timeline: Continuous writers, readers waiting Time: 0 1 2 3 4 5 6 7 8 ... |----|----|----|----|----|----|----|----|----| ... W1: [====] Writer 1W2: [====] Writer 2 W3: [====] Writer 3W4: [====] Writer 4... write_count: 1 1 1 1 1 1 1 1 ... (never 0!)read_try: 0 0 0 0 0 0 0 0 ... (always held) R1: [BLOCKED on read_try-----------------------------→ StarvingR2: [BLOCKED on read_try-----------------------------→ StarvingStarvation Condition:
Readers starve when: after blocking on wait(read_try), there exists an infinite execution trace where write_count never becomes 0.
This occurs when:
- Writers continuously arrive (faster than they complete)
- Each arriving writer finds
write_count ≥ 1 read_tryis never signaled- Readers wait indefinitely
Probability of Starvation:
Starvation probability depends on:
- Writer arrival rate vs. write completion rate
- Whether writers batch or spread over time
- System policies for scheduling writers
| Workload Pattern | Starvation Risk | Mitigation Strategy |
|---|---|---|
| Low writer rate | Low | None needed |
| Bursty writes with gaps | Low | Readers proceed during gaps |
| Steady moderate writes | Medium | Monitor reader latency |
| High sustained writes | High | Consider fair solution |
| Write-storm events | Very High | Rate limit or queue writes |
Reader starvation can cause user-visible failures: timeouts on read requests, 'service unavailable' errors, and degraded application performance. Systems using writer priority must monitor reader latency and have fallback strategies for write-storm scenarios.
Performance Characteristics
The Writers Preference solution has different performance characteristics than Readers Preference.
Overhead Analysis:
| Operation | Wait Operations | Signal Operations | Notes |
|---|---|---|---|
| Reader Entry | 3 (read_try, mutex, possibly rw_mutex) | 2 (mutex, read_try) | More overhead than RP |
| Reader Exit | 1 (mutex) | 1-2 (mutex, possibly rw_mutex) | Similar to RP |
| Writer Entry | 2 (write_mutex, possibly read_try) + 1 (rw_mutex) | 1 (write_mutex) | More overhead than RP |
| Writer Exit | 1 (write_mutex) | 2-3 | More overhead than RP |
Critical Observations:
-
Reader entry is more expensive: Readers must acquire
read_try(which may block), thenmutex, potentiallyrw_mutex, then releasemutexandread_try. This is 3-5 semaphore operations vs. 2-3 in Readers Preference. -
read_try becomes a bottleneck: Unlike Readers Preference where readers only contend on
mutex, here all readers serialize throughread_tryat entry AND exit. This reduces reader concurrency even when no writers are waiting. -
Writer latency is bounded: Writers wait at most for active readers to finish. They're guaranteed to proceed after current readers exit.
Throughput Impact:
For read-heavy workloads, Writers Preference has lower throughput than Readers Preference because:
- Extra semaphore operations per reader
- Partial serialization through
read_try - Writers interrupt reader batches
However, for write-heavy workloads, Writers Preference may have better throughput because writers don't starve—they proceed efficiently.
- •Writers never starve
- •Bounded writer latency
- •Write-sensitive data stays fresh
- •Good for write-heavy workloads
- •Predictable write performance
- •Reader starvation possible
- •Higher synchronization overhead
- •Reduced reader concurrency
- •More complex implementation
- •Lower read throughput
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.