Loading content...
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.
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.
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:
writer_waiting simultaneously with a writer setting itRequired Additional State:
The Writers Preference solution needs:
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.
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.
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.
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:
rw_mutex during their critical sectionrw_mutex before enteringrw_mutex, the first reader blocks at wait(rw_mutex)rw_mutex, writers block at wait(rw_mutex)rw_mutex enforces 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:
rw_mutex before entering critical sectionrw_mutex is a binary semaphoreTheorem 3: Reader Concurrency Statement: Multiple readers can be in the critical section simultaneously.
Proof:
rw_mutexread_count > 1 and skip wait(rw_mutex)read_try semaphore is acquired and immediately released by each readerread_try sequentially and all enter the critical sectionmutex only serializes access to read_count, not to the resourceTheorem 4: Writer Priority (No Writer Starvation) Statement: A waiting writer eventually enters the critical section.
Proof:
wait(write_mutex); write_count++, it's registered as waitingread_tryread_try held, no new readers can start (they block on wait(read_try))rw_mutexrw_mutex and entersThe '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.
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:
write_count ≥ 1read_try is never signaledProbability of Starvation:
Starvation probability depends on:
| 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.
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), then mutex, potentially rw_mutex, then release mutex and read_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 through read_try at 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:
read_tryHowever, for write-heavy workloads, Writers Preference may have better throughput because writers don't starve—they proceed efficiently.
Understanding when to choose Writers Preference over Readers Preference requires careful analysis of requirements.
Head-to-Head Comparison:
| Property | Readers Preference | Writers Preference |
|---|---|---|
| Semaphores Required | 2 (mutex, rw_mutex) | 4 (mutex, write_mutex, rw_mutex, read_try) |
| Reader Entry Overhead | Low (2-3 ops) | Higher (3-5 ops) |
| Writer Entry Overhead | 1 wait | 2-3 waits |
| Max Concurrent Readers | Unbounded | Still unbounded (but serialized entry) |
| Reader Starvation | No | Yes (under writer load) |
| Writer Starvation | Yes (under reader load) | No |
| Implementation Complexity | Simple | Moderate |
| Best Throughput For | Read-heavy | Balanced/Write-heavy |
Decision Framework:
Choose Readers Preference when:
Choose Writers Preference when:
If both reader and writer starvation are unacceptable, neither Readers Preference nor Writers Preference is appropriate. Consider Fair (FIFO) solutions that guarantee bounded waiting for all threads. We'll explore these in the next page.
Implementing Writers Preference correctly requires attention to subtle details.
Avoiding Common Bugs:
1234567891011121314151617181920212223242526272829303132333435363738
// BUG 1: Forgetting to release read_try immediatelyvoid reader_buggy() { wait(read_try); // Acquire gate wait(mutex); read_count++; if (read_count == 1) wait(rw_mutex); signal(mutex); // BUG: Forgot signal(read_try) here! // Result: Only one reader can ever enter read_data(); // ...} // BUG 2: Wrong order of operations in writervoid writer_buggy() { wait(rw_mutex); // BUG: Get resource access first wait(write_mutex); // Then update write_count write_count++; if (write_count == 1) wait(read_try); // Too late! Already blocked readers elsewhere signal(write_mutex); // Result: Writers don't properly block readers} // BUG 3: Releasing read_try in reader exitvoid reader_exit_buggy() { wait(mutex); read_count--; if (read_count == 0) { signal(rw_mutex); signal(read_try); // BUG: Reader shouldn't touch read_try in exit } signal(mutex); // Result: read_try signaled when there are still waiting writers}Testing for Correctness:
The increased complexity of Writers Preference makes thorough testing essential:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// Test: Writers actually get priorityvoid test_writer_priority() { // Start a reader that holds for a while pthread_create(&t1, NULL, long_reader, NULL); usleep(100); // Ensure reader is active // Start a writer uint64_t writer_queue_time = get_time_ns(); pthread_create(&t2, NULL, writer, NULL); usleep(50); // Writer is now waiting // Start another reader atomic_bool reader2_started = false; pthread_create(&t3, NULL, reader_with_flag, &reader2_started); // Wait for first reader to finish pthread_join(t1, NULL); // Check: writer should complete before reader2 starts pthread_join(t2, NULL); uint64_t writer_done_time = get_time_ns(); assert(!reader2_started || reader2_started_after(writer_done_time)); printf("Writer priority verified\n");} // Test: No starvation under moderate loadvoid test_no_immediate_starvation() { atomic_bool writer_completed = false; // Start readers continuously for 100ms stop_readers = false; for (int i = 0; i < 10; i++) pthread_create(&readers[i], NULL, continuous_reader, NULL); usleep(10000); // Let readers establish // Start a writer pthread_create(&writer_thread, NULL, timed_writer, &writer_completed); // Writer should complete within reasonable time // (unlike Readers Preference where it might never complete) usleep(500000); // 500ms assert(writer_completed && "Writer should have completed!"); stop_readers = true;}If a thread that holds read access needs to upgrade to write access, it cannot do so directly in Writers Preference. The thread would block on rw_mutex (held by itself as a reader) waiting for itself to exit. Upgradable locks require special mechanisms not present in the basic solution.
We have thoroughly examined the Writers Preference (Second Readers-Writers Problem) solution. Let's consolidate the key concepts:
What's Next:
Both Readers Preference and Writers Preference suffer from starvation—just of different classes. The Fair Solution (Third Readers-Writers Problem) eliminates starvation for both readers and writers by using FIFO ordering. The next page develops fair solutions that guarantee bounded waiting for all threads, exploring the tradeoffs this fairness requires.
You now have complete mastery of the Writers Preference solution: its motivation, implementation, correctness proofs, starvation characteristics, and comparison with Readers Preference. This prepares you for understanding fair solutions that eliminate starvation entirely.