04. Classical Synchronization Problems

In concurrent systems engineering, theoretical concepts like mutual exclusion, semaphores, and condition variables are evaluated against a battery of classical archetypes.

These classical problems are not merely intellectual puzzles: they encapsulate real-world architectural challenges found in UNIX pipe implementations, network socket buffers, database transaction engines, and multiprocessor operating system kernels.

1. The Bounded-Buffer (Producer-Consumer) Problem

The Bounded-Buffer Problem (originally formulated by Edsger W. Dijkstra) is the definitive architectural archetype for asynchronous decoupled systems.

In modern computing, execution threads rarely operate in lockstep. A network card receives ethernet frames at gigabit line rate, an operating system audio server generates PCM audio buffers, and a database storage engine writes transaction log entries asynchronously to NVMe storage. In each scenario, an execution thread producing data must coordinate with an independent execution thread consuming data through a fixed-size storage buffer in memory.

Formal Problem Specification & System Invariants

The architecture consists of one or more producers and one or more consumers sharing a circular ring buffer partitioned into N discrete slots:

Figure 4.1: Circular Ring Buffer & Tri-Semaphore Architecture
How counting semaphores track vacant and populated slots while a binary semaphore serializes ring pointer updates.
CIRCULAR BUFFER RING (Capacity N = 5) SLOT 0 [Item 0] FULL SLOT 1 [Item 1] FULL SLOT 2 [Item 2] FULL SLOT 3 [Empty] VACANT SLOT 4 [Empty] VACANT head pointer = 3 ↑ Next Producer Write tail pointer = 0 ↑ Next Consumer Read empty (2) + full (3) = N (5) [CONSERVED] TRI-SEMAPHORE STATE COUNTING: empty = 2 Tracks vacant slots • Producer down(&empty) Blocks producer when empty == 0 COUNTING: full = 3 Tracks populated slots • Consumer down(&full) Blocks consumer when full == 0 BINARY: mutex = 1 (FREE) Enforces exclusive critical section access Guards ring pointers and buffer arrays

The Tri-Semaphore Architecture

To solve the bounded-buffer problem without busy waiting, Dijkstra established the canonical tri-semaphore architecture:

Semaphore Initialization and Invariants:
semaphore_t mutex; /* Binary semaphore: initialized to 1 (Buffer locked = 0, free = 1) */
semaphore_t empty; /* Counting semaphore: initialized to N (Vacant slots available) */
semaphore_t full;  /* Counting semaphore: initialized to 0 (Populated items available) */
The Fundamental Conservation Law:
At any instant in time, the sum of vacant slots and filled items must strictly equal the buffer capacity:
empty + full = N

Complete POSIX C Implementation

Below is the complete, industrial-grade implementation using POSIX semaphores (semaphore.h):

/* Production Bounded-Buffer Implementation Using POSIX Semaphores */
#include <semaphore.h>
#include <pthread.h>
#include <stdbool.h>

#define BUFFER_CAPACITY 8

typedef struct {
    int data[BUFFER_CAPACITY];
    int head;                  /* Index for next insert: head = (head + 1) % N */
    int tail;                  /* Index for next remove: tail = (tail + 1) % N */
    sem_t mutex;               /* Binary semaphore guarding buffer pointers */
    sem_t empty;               /* Counting semaphore tracking vacant slots */
    sem_t full;                /* Counting semaphore tracking available items */
} bounded_buffer_t;

void buffer_init(bounded_buffer_t *b) {
    b->head = 0;
    b->tail = 0;
    sem_init(&b->mutex, 0, 1);               /* Binary mutex: init 1 */
    sem_init(&b->empty, 0, BUFFER_CAPACITY); /* Empty slots: init N */
    sem_init(&b->full,  0, 0);               /* Full items: init 0 */
}

void buffer_produce(bounded_buffer_t *b, int item) {
    sem_wait(&b->empty); /* 1. Decrement empty slots (Blocks if buffer full!) */
    sem_wait(&b->mutex); /* 2. Acquire critical section lock */

    /* Critical Section: Insert item into circular ring */
    b->data[b->head] = item;
    b->head = (b->head + 1) % BUFFER_CAPACITY;

    sem_post(&b->mutex); /* 3. Release critical section lock */
    sem_post(&b->full);  /* 4. Increment full items (Wakes waiting consumer) */
}

int buffer_consume(bounded_buffer_t *b) {
    sem_wait(&b->full);  /* 1. Decrement full items (Blocks if buffer empty!) */
    sem_wait(&b->mutex); /* 2. Acquire critical section lock */

    /* Critical Section: Extract item from circular ring */
    int item = b->data[b->tail];
    b->tail = (b->tail + 1) % BUFFER_CAPACITY;

    sem_post(&b->mutex); /* 3. Release critical section lock */
    sem_post(&b->empty); /* 4. Increment empty slots (Wakes waiting producer) */

    return item;
}

The Deadlock Inversion Trap & Resource Allocation Graphs

In concurrent algorithm design, the order of lock acquisition dictates system liveness.

Consider the catastrophic defect that occurs if an inexperienced developer swaps the order of the two wait calls in the producer routine:

/* FATAL: Inverted Lock Acquisition Ordering */
void flawed_producer(bounded_buffer_t *b, int item) {
    sem_wait(&b->mutex); /* STEP 1: Claim mutex FIRST (Catastrophic Error!) */
    sem_wait(&b->empty); /* STEP 2: Check empty slot availability */

    b->data[b->head] = item;
    b->head = (b->head + 1) % BUFFER_CAPACITY;

    sem_post(&b->mutex);
    sem_post(&b->full);
}
Anatomy of the Inversion Deadlock:
  1. Suppose the buffer is full (empty == 0).
  2. The Producer executes sem_wait(&b->mutex). Because mutex == 1, the Producer acquires the lock (mutex drops to 0).
  3. The Producer executes sem_wait(&b->empty). Because empty == 0, the Producer is suspended and moved to the empty wait queue. Crucially, the Producer is put to sleep while still holding the mutex!
  4. The Consumer is scheduled. To free up a buffer slot, it must consume an item. It executes sem_wait(&b->full) (which succeeds, as items exist).
  5. The Consumer then executes sem_wait(&b->mutex). Because the sleeping Producer holds mutex, the Consumer blocks immediately!
The Circular Wait Dependency (Deadlock):
The Producer holds mutex and is waiting for empty (which can only be produced by the Consumer).
The Consumer holds full and is waiting for mutex (which is held by the Producer).
[Producer] → (holds mutex) → [blocks on empty] ← (produced by Consumer)
   ↑                                                      ↓
   ±——————————— [Consumer blocks on mutex] ←———————————±
Neither thread can ever make progress. The system is permanently locked.

The Win32 Windows Alternative

On Windows, the Bounded-Buffer problem can be implemented using native Win32 Kernel Semaphore Handles:

/* Windows Win32 Bounded-Buffer Implementation */
#include <windows.h>

typedef struct {
    int data[BUFFER_CAPACITY];
    int head, tail;
    HANDLE hMutex; /* Mutex object or CRITICAL_SECTION */
    HANDLE hEmpty; /* Counting semaphore: init N, max N */
    HANDLE hFull;  /* Counting semaphore: init 0, max N */
} win_bounded_buffer_t;

void win_produce(win_bounded_buffer_t *b, int item) {
    WaitForSingleObject(b->hEmpty, INFINITE); /* 1. Wait for vacant slot */
    WaitForSingleObject(b->hMutex, INFINITE); /* 2. Acquire buffer lock */

    b->data[b->head] = item;
    b->head = (b->head + 1) % BUFFER_CAPACITY;

    ReleaseMutex(b->hMutex);                  /* 3. Release buffer lock */
    ReleaseSemaphore(b->hFull, 1, NULL);      /* 4. Signal populated item */
}

Interactive Stepper: Bounded-Buffer Lock Inversion Deadlock vs. Correct Ordering

Execution Scenario Buffer of capacity N=2 is FULL (count=2, empty=0, full=2). Producer attempts to insert a new item. Demonstrating how acquiring mutex before checking empty slots triggers complete deadlock.
Timeline Clock T = 0 ms
Active Thread Producer
Mutex Lock (0/1) mutex = 1 (FREE)
Empty Slots empty = 0 (FULL!)
System State Nominal
PRODUCER THREAD STATE: RUNNING NEXT INSTRUCTION: down(&mutex) LOCKS HELD: [None] BUFFER RING & SEMAPHORES Slot 0 [ITEM A] Slot 1 [ITEM B] SEMAPHORE TELEMETRY: mutex = 1 empty = 0 (Buffer Full) full = 2 (2 Items Ready) CONSUMER THREAD STATE: READY NEXT INSTRUCTION: down(&mutex) WAITING FOR: [Ready for dispatch]
Current Step Summary Buffer is FULL (N=2, empty=0, full=2). Producer attempts to add an item. In this flawed implementation, Producer calls down(&mutex) FIRST.
What Is Happening

Producer runs down(&mutex) and claims the critical section lock.

Why The System Does This

Lock inversion defect: Claiming mutual exclusion before verifying resource capacity traps the lock in a blocked thread.

2. The Dining Philosophers Problem

Originally formulated by Edsger W. Dijkstra in 1965 as an examination problem on synchronizing tape drives, and later refined into its allegorical form by C. A. R. Hoare, the Dining Philosophers Problem is the definitive paradigm for resource allocation deadlocks in multi-threaded operating systems.

The problem models concurrent processes competing for limited, mutually exclusive non-preemptible resources (such as multiple disk spindles, database row locks, or memory channels).

Formal Specification

Five philosophers sit around a circular dining table:

Figure 4.2: Dining Philosophers Circular Topology & The Circular Wait Hazard
How simultaneous left-hand acquisition establishes the fatal circular dependency ring.
SPAGHETTI P0 P1 P2 P3 P4 C0 C1 C2 C3 C4 THE COFFMAN DEADLOCK CONDITIONS 1. Mutual Exclusion: Chopsticks are non-shareable: exactly one philosopher per utensil. 2. Hold and Wait: Philosopher holds their left chopstick while waiting for right. 3. No Preemption: Utensils cannot be forcibly confiscated from an adjacent peer. 4. Circular Wait (The Fatal Ring): P0 → C1 → P1 → C2 → P2 → C3 → P3 → C4 → P4 → C0 → P0 Closed directed cycle in Resource Allocation Graph!

The Naive Implementation & The Coffman Deadlock Conditions

The naive solution maps each chopstick to an individual binary semaphore (chopstick[5], each initialized to 1). Each philosopher runs an identical, symmetric routine:

/* FLAWED: Naive Symmetric Philosopher Routine */
sem_t chopstick[5]; /* Initialized to 1 */

void philosopher_naive(int i) {
    while (true) {
        think();
        sem_wait(&chopstick[i]);             /* 1. Pick up left chopstick */
        sem_wait(&chopstick[(i + 1) % 5]);   /* 2. Pick up right chopstick */

        eat();

        sem_post(&chopstick[i]);             /* 3. Put down left chopstick */
        sem_post(&chopstick[(i + 1) % 5]);   /* 4. Put down right chopstick */
    }
}
Why the Naive Solution Triggers Total Deadlock:
Suppose all five philosophers become hungry simultaneously. Each philosopher executes statement 1, grabbing their left chopstick: Now, every philosopher executes statement 2, attempting to acquire their right chopstick: This satisfies all four Coffman Conditions: Mutual Exclusion, Hold-and-Wait, No-Preemption, and Circular Wait. The system deadlocks permanently, and all five philosophers starve.

Strategy 1: Asymmetric Resource Hierarchy (Dijkstra's Symmetry Breaking)

Deadlock can only occur if a closed circular dependency cycle exists. Dijkstra proved that establishing a strict total ordering over all resources mathematically guarantees freedom from deadlock.

Assign each chopstick a global numerical index from 0 to 4. Establish a rule: every philosopher must always acquire their lower-numbered chopstick first, and their higher-numbered chopstick second:

/* Strategy 1: Asymmetric Symmetry Breaking */
void philosopher_asymmetric(int i) {
    int left = i;
    int right = (i + 1) % 5;

    while (true) {
        think();

        if (i % 2 == 0) {
            /* Even philosophers: Left first, then Right */
            sem_wait(&chopstick[left]);
            sem_wait(&chopstick[right]);
        } else {
            /* Odd philosophers: Right first, then Left */
            sem_wait(&chopstick[right]);
            sem_wait(&chopstick[left]);
        }

        eat();

        sem_post(&chopstick[left]);
        sem_post(&chopstick[right]);
    }
}
Proof of Deadlock Freedom under Resource Hierarchy:
Suppose all five philosophers become hungry simultaneously:
  1. Philosopher 0 and Philosopher 4 both compete for Chopstick 0 as their very first acquisition.
  2. Exactly one of them wins (say, Philosopher 0 claims Chopstick 0).
  3. Philosopher 4 blocks immediately on Chopstick 0 before claiming Chopstick 4!
  4. Because Chopstick 4 is left untouched, Philosopher 3 successfully claims both Chopstick 3 and Chopstick 4, eats, and releases both.
The circular dependency cycle is broken. At least one philosopher is always guaranteed to eat.

Strategy 2: Tanenbaum's State-Tracking Semaphore Array

While the asymmetric approach prevents deadlock, it can introduce unequal waiting times. Andrew Tanenbaum designed an elegant solution utilizing an explicit state machine array that allows a philosopher to eat only when neither neighbor is eating:

/* Strategy 2: Tanenbaum's State-Tracking Solution */
#define N 5
#define LEFT  ((i + N - 1) % N)
#define RIGHT ((i + 1) % N)

typedef enum { THINKING, HUNGRY, EATING } state_t;

state_t state[N];         /* Tracks state of every philosopher */
sem_t mutex;              /* Binary mutex protecting state[] inspections */
sem_t s[N];               /* One private semaphore per philosopher (init 0) */

void test_neighbors(int i) {
    /* If I am hungry AND neither neighbor is eating, begin eating */
    if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING) {
        state[i] = EATING;
        sem_post(&s[i]);  /* Wake myself up (increments s[i] from 0 to 1) */
    }
}

void take_forks(int i) {
    sem_wait(&mutex);     /* Enter critical section */
    state[i] = HUNGRY;
    test_neighbors(i);    /* Attempt to claim both forks */
    sem_post(&mutex);     /* Exit critical section */

    sem_wait(&s[i]);      /* Block if forks were not available */
}

void put_forks(int i) {
    sem_wait(&mutex);     /* Enter critical section */
    state[i] = THINKING;
    test_neighbors(LEFT); /* Check if left neighbor can now eat */
    test_neighbors(RIGHT);/* Check if right neighbor can now eat */
    sem_post(&mutex);     /* Exit critical section */
}
Key Insights of Tanenbaum's Solution:

Strategy 3: Windows WaitForMultipleObjects

On Windows NT platforms, the dining philosophers problem can be resolved using the kernel's native multi-handle wait capability:

/* Win32 Atomic Multi-Object Fork Acquisition */
#include <windows.h>

HANDLE hChopsticks[5]; /* Mutex handles for each utensil */

void philosopher_win32(int i) {
    HANDLE needed[2];
    needed[0] = hChopsticks[i];
    needed[1] = hChopsticks[(i + 1) % 5];

    while (true) {
        think();

        /* Atomically wait for BOTH mutexes simultaneously (bWaitAll = TRUE) */
        DWORD result = WaitForMultipleObjects(2, needed, TRUE, INFINITE);
        if (result == WAIT_OBJECT_0) {
            eat();
            ReleaseMutex(needed[0]);
            ReleaseMutex(needed[1]);
        }
    }
}

Because WaitForMultipleObjects(..., bWaitAll = TRUE) is an atomic kernel-level operation, the operating system scheduler claims both handles together. If either utensil is held, the thread is suspended without claiming the other, eliminating the "Hold and Wait" Coffman condition.

3. The Readers-Writers Problem

Formulated by P. J. Courtois, F. Heymans, and D. L. Parnas in 1971, the Readers-Writers Problem models concurrent access to a shared resource—such as an in-memory database table, an operating system routing table, or a file system directory cache—where competing threads have asymmetric access requirements:

The Concurrency Matrix

Active Entity Second Reader Arrives Second Writer Arrives
Reader Active ✓ PERMITTED (Concurrent Reads) × BLOCKED (Writer Must Wait)
Writer Active × BLOCKED (Reader Must Wait) × BLOCKED (Exclusive Access Only)
Figure 4.3: Readers-Writers Database Access Modes and Fair Turnstile Queuing
How an entry turnstile enforces FIFO queueing, preventing continuous reader arrivals from starving waiting writers.
ENTRY TURNSTILE GATE sem_t turnstile (Init 1) Enforces FIFO arrival order THREAD ARRIVAL SEQUENCE: Reader R1 → In Chamber Writer W1 → Queued at Gate Reader R2 → Blocked Behind W1 SHARED DATABASE CHAMBER SHARED READ CONCURRENCY: Reader 1 Reader 2 Reader 3 EXCLUSIVE WRITE MUTEX: WRITER (SOLE OCCUPANT) SYNCHRONIZATION STATE READER COUNTER: read_count = 1 RESOURCE LOCK: db_mutex = 0 (HELD) FAIRNESS POLICY: ✓ Fair Turnstile Active

The Three Classical Variations

The core challenge in solving the Readers-Writers problem is not merely achieving mutual exclusion, but establishing an equitable priority policy between readers and writers.

1. First Readers-Writers (Reader-Preference)

In this classic implementation, readers are given priority over writers. If the database is currently open for reading, any newly arrived reader is permitted to enter immediately, regardless of whether a writer is waiting:

/* First Readers-Writers (Reader-Preference) */
#include <semaphore.h>

int read_count = 0;
sem_t count_mutex; /* Binary semaphore protecting read_count (init 1) */
sem_t db_mutex;    /* Binary semaphore protecting database access (init 1) */

void reader(void) {
    sem_wait(&count_mutex);
    read_count++;
    if (read_count == 1) {
        sem_wait(&db_mutex);   /* First reader locks database against writers */
    }
    sem_post(&count_mutex);

    read_database();           /* Reading is executed concurrently */

    sem_wait(&count_mutex);
    read_count--;
    if (read_count == 0) {
        sem_post(&db_mutex);   /* Last reader unlocks database for writers */
    }
    sem_post(&count_mutex);
}

void writer(void) {
    sem_wait(&db_mutex);       /* Exclusive lock: blocks all readers and writers */
    write_database();
    sem_post(&db_mutex);
}
The Pathological Defect: Indefinite Writer Starvation
Suppose Reader 1 enters and claims db_mutex. While Reader 1 is reading, a Writer arrives and blocks on sem_wait(&db_mutex).
An instant later, Reader 2 arrives. Because read_count is already 1, Reader 2 increments read_count to 2 and enters without touching db_mutex.
Before Reader 2 finishes, Reader 3 arrives. Then Reader 4 arrives. As long as a continuous stream of overlapping readers arrives, read_count never drops to 0.
Result: The writer waits indefinitely in the queue, suffering permanent starvation (violating Condition 4: Bounded Waiting).
2. Second Readers-Writers (Writer-Preference)

To prevent writer starvation and ensure database updates are not delayed by an endless stream of reads, Courtois et al. designed the Writer-Preference variant:

3. Third Readers-Writers (Fair FIFO Turnstile / No Starvation)

To achieve genuine fairness where neither readers nor writers starve, the algorithm introduces an architectural Turnstile Semaphore (turnstile, initialized to 1):

/* Third Readers-Writers (Fair Turnstile: Starvation-Free) */
int read_count = 0;
sem_t count_mutex; /* Protects read_count (init 1) */
sem_t db_mutex;    /* Exclusive access to database (init 1) */
sem_t turnstile;   /* Fair entry turnstile gate (init 1) */

void fair_reader(void) {
    sem_wait(&turnstile);      /* 1. Pass through entry turnstile */
    sem_wait(&count_mutex);
    read_count++;
    if (read_count == 1) {
        sem_wait(&db_mutex);   /* First reader locks database */
    }
    sem_post(&count_mutex);
    sem_post(&turnstile);      /* 2. Release turnstile for next thread in queue */

    read_database();

    sem_wait(&count_mutex);
    read_count--;
    if (read_count == 0) {
        sem_post(&db_mutex);   /* Last reader unlocks database */
    }
    sem_post(&count_mutex);
}

void fair_writer(void) {
    sem_wait(&turnstile);      /* 1. Claim turnstile gate: BLOCKS new readers */
    sem_wait(&db_mutex);       /* 2. Await active readers to drain; claim database */

    write_database();

    sem_post(&turnstile);      /* 3. Release turnstile gate */
    sem_post(&db_mutex);       /* 4. Release database */
}
Why the Turnstile Prevents Writer Starvation:
When a writer arrives:
  1. The writer executes sem_wait(&turnstile) and claims the turnstile gate.
  2. If newly arriving readers appear, they attempt to execute sem_wait(&turnstile) and block immediately at the gate behind the writer!
  3. The readers currently inside the chamber complete their reads and exit. The last reader sets read_count = 0 and executes sem_post(&db_mutex).
  4. The writer acquires db_mutex, writes to the database, and releases both locks.
Readers can no longer jump ahead of an already waiting writer. Both readers and writers experience strictly bounded waiting times.

Production Systems: POSIX pthread_rwlock_t

In POSIX C systems programming, developers rarely write raw semaphore turnstiles. Instead, the runtime provides native, highly optimized Read-Write Locks (pthread_rwlock_t):

/* Production POSIX Read-Write Lock Usage */
#include <pthread.h>

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

void posix_reader(void) {
    pthread_rwlock_rdlock(&rwlock); /* Shared lock: multiple readers */
    read_database();
    pthread_rwlock_unlock(&rwlock);
}

void posix_writer(void) {
    pthread_rwlock_wrlock(&rwlock); /* Exclusive lock: single writer */
    write_database();
    pthread_rwlock_unlock(&rwlock);
}

Under the GNU C Library (glibc) on Linux, developers can configure the scheduling attribute via pthread_rwlockattr_setkind_np:

The Linux Kernel Revolution: Read-Copy-Update (RCU)

In modern multi-core operating system kernels, traditional reader-writer locks have a fatal performance flaw: cacheline bouncing. Every time a reader acquires rwlock, it must atomically increment an internal reader counter. This atomic write forces cacheline invalidations across all CPU cores, degrading memory bus throughput.

To solve this, the Linux kernel relies heavily on Read-Copy-Update (RCU):

Windows Readers-Writers & Dispatcher Objects

In the Windows NT architecture, thread synchronization spans two distinct layers: Kernel Dispatcher Objects and lightweight User-Mode SRW Locks.

1. Slim Reader/Writer (SRW) Locks

The Windows Win32 API provides native support for the Readers-Writers problem via SRWLOCK:

/* Windows SRWLock Implementation (Clean & Fast) */
#include <windows.h>

SRWLOCK rw_lock = SRWLOCK_INIT; /* Pointer-sized allocation */

void read_worker(void) {
    AcquireSRWLockShared(&rw_lock); /* Shared access for multiple readers */
    read_database();
    ReleaseSRWLockShared(&rw_lock);
}

void write_worker(void) {
    AcquireSRWLockExclusive(&rw_lock); /* Exclusive access for writers */
    write_database();
    ReleaseSRWLockExclusive(&rw_lock);
}

Unlike POSIX reader-writer locks which can be prone to recursion bugs, SRW locks are explicitly non-reentrant. Acquiring an exclusive SRW lock recursively will trigger a dead stop or access failure, dramatically simplifying internal lock state and reducing the structure to a single machine pointer.

2. Windows Kernel Dispatcher Objects & WaitForMultipleObjects

When synchronization must cross process boundaries or coordinate complex multi-resource graphs, Windows uses Kernel Dispatcher Objects:

Interactive Stepper: Readers-Writers Starvation vs. Fair Turnstile

Policy Simulation Reader R1 is currently reading the database. Writer W1 arrives to submit an update. Under Reader-Preference, continuous overlapping reader arrivals starve W1 permanently.
Timeline Clock T = 0 ms
Active Readers Inside read_count = 1
Writer Queue State W1 Awaiting Entry
Database Access Mode SHARED_READ
Fairness Metric Nominal
ENTRY TURNSTILE GATE GATE STATUS: OPEN Readers pass freely PENDING ARRIVAL QUEUE: Reader R2 (Arrived T=1) Writer W1 (Blocked T=1) Reader R3 (Arrived T=2) SHARED DATABASE CHAMBER CURRENT MODE: SHARED READ Active: [R1] read_count = 1 | db_mutex HELD SYNCHRONIZATION STATE: db_mutex=0 (Held by Readers) WRITER W1 STATUS WRITER STARVING! Blocked on db_mutex FAIRNESS METRICS: Wait Time: 0 ms Readers Bypassed: 0 Starvation Risk: HIGH
Current Step Summary Initial State: Reader R1 is inside the database. Writer W1 and Reader R2 arrive simultaneously.
What Is Happening

Reader R1 holds db_mutex. Writer W1 and Reader R2 arrive at the gate.

Why The System Does This

Baseline setup demonstrating how different scheduling policies arbitrate competing reader and writer queues.