A comprehensive simulation of the classic Reader-Writer Synchronization Problem using POSIX threads, mutexes, and semaphores for coordinated resource access.
The Reader-Writer Problem is a fundamental synchronization challenge in operating systems that demonstrates how to manage concurrent access to a shared resource.
Imagine a shared library with multiple visitors:
- Readers can enter and read books simultaneously (multiple readers don't interfere with each other).
- Writers must have exclusive access (no other reader or writer can be present).
- The Goal: Allow maximum concurrency while ensuring data consistency and preventing race conditions.
- Read-Write Lock: A synchronization mechanism that permits multiple concurrent readers but only one writer.
- Mutual Exclusion: Ensures that only one writer (or no readers when a writer is active) can access the resource.
- Starvation Prevention: Both readers and writers should eventually get their turn.
- Fairness: The system should balance between reader and writer priorities based on the chosen variant.
This implementation uses POSIX threading libraries to demonstrate synchronization primitives in action.
The program creates multiple threads:
- Reader threads: Simulate processes reading from a shared resource.
- Writer threads: Simulate processes writing to a shared resource.
- Each thread has a unique ID, execution time, and synchronized access pattern.
pthread_mutex_t counter_mutex;- Protects the
read_countvariable from race conditions. - Ensures atomic operations when incrementing/decrementing reader count.
- Prevents simultaneous modification of shared state.
sem_t writer_sem;- A binary semaphore initialized to 1 (unlocked state).
- Controls exclusive access for writers.
- The first reader acquires the semaphore; the last reader releases it.
- Writers must acquire it before writing.
read_count: Tracks the number of active readers.counter_mutex: Protectsread_countfrom race conditions.writer_sem: Guards write access and blocks writers when readers are active.
- Acquire mutex to safely check and increment
read_count. - If first reader (read_count was 0), acquire the writer semaphore to block all writers.
- Release mutex to allow other readers to enter.
- Read the resource (simulated by
sleep()). - Re-acquire mutex to safely decrement
read_count. - If last reader (read_count becomes 0), release the writer semaphore to unblock waiting writers.
- Release mutex and terminate.
- Acquire writer semaphore (blocks if any reader or another writer is active).
- Write the resource (simulated by
sleep()). - Release writer semaphore to unblock waiting readers or writers.
- Terminate.
- Multiple readers can execute concurrently.
- Only one writer can execute at a time.
- No reader and writer execute simultaneously.
- A C compiler (gcc, clang, or MSVC)
- POSIX thread library (
pthread.h) - Standard C libraries (
stdio.h,stdlib.h,unistd.h)
- MinGW or WSL2 for POSIX thread support
- Or use MSVC with platform-specific adjustments
- POSIX threads are built-in
gcc main.c -o reader_writer -pthreadgcc main.c -o reader_writer.exe -pthreadgcc main.c -o reader_writer -pthread
./reader_writer./reader_writer.\reader_writer.exeWhen executed, the program displays interleaved reader and writer actions:
WE provided four process:
three of which are readers and one is a writer
and arrived at the same time at 0s
Reader 0 is reading for 5s...
Reader 1 is reading for 8s...
Blocking any Writing processes, Because ....
Reader 2 is reading for 3s...
Reader 2 has finished.
Reader 0 has finished.
Reader 1 has finished.
Notifying Writers to commit, Because .....
Writer 3 is Writing for 6s...
Writer 3 has finished.
- "Blocking any Writing processes...": The first reader acquired the writer semaphore.
- Multiple readers execute concurrently: Notice readers 0, 1, and 2 all start without blocking each other.
- "Notifying Writers to commit...": The last reader released the writer semaphore.
- Writer executes exclusively: Only after all readers finish, the writer begins.
- The current implementation uses the "readers-preference" variant.
- Once a reader arrives, incoming readers can proceed without waiting for the writer.
- Potential Issue: Writers may starve if readers keep arriving.
read_countis protected bycounter_mutexto prevent race conditions.- Ensures accurate tracking of active readers.
writer_semacts as a turnstile for writers.- The first reader blocks writers; the last reader unblocks them.
- Thread IDs (0, 1, 2, 3) provide predictable process identification.
- Execution times are statically defined for reproducibility.
- All threads are joined before destroying synchronization primitives.
- Prevents resource leaks and undefined behavior.
Modify the loop in main():
#define NUM_PROCESSES 4Then adjust array sizes and reader/writer distribution.
Update the execution_time array in main.c:
int execution_time[4] = {5, 8, 3, 6}; // execution times in secondsModify the condition in the thread creation loop:
if (i == 3) { // Change 3 to 2 for two writers, etc.
pthread_create(&process_threads[i], NULL, writer, id);
} else {
pthread_create(&process_threads[i], NULL, reader, id);
}To implement the "writer-preference" solution (preventing writer starvation):
- Add a second semaphore
read_semto block arriving readers when a writer is waiting. - Track
write_count(number of waiting writers). - Modify reader entry/exit logic to check for waiting writers.
- Without mutex: Two readers could both see
read_count == 0, both acquirewriter_sem, causing inconsistency. - With mutex: Only one reader checks and modifies
read_countat a time.
sem_wait(): Decrements the semaphore; blocks if it reaches 0.sem_post(): Increments the semaphore; wakes a waiting thread if any exist.
- Database systems: Multiple readers, exclusive writers (journaling).
- Cache management: Reader threads query; writer threads invalidate.
- Configuration servers: Many clients read; few services write updates.
This project demonstrates the core synchronization challenges in concurrent programming:
- How mutexes protect critical sections.
- How semaphores enforce mutual exclusion and ordering.
- The importance of careful synchronization to prevent deadlock and race conditions.
It is a strong foundation for extending into:
- Writer-preference or reader-preference analysis: Compare starvation behavior.
- Multiple shared resources: Extend to a read-write lock library.
- Performance analysis: Measure throughput under different workloads.
- Deadlock detection: Add timeout mechanisms and circular wait detection.
- Monitor pattern: Implement using condition variables instead of semaphores.