Hands-On Multithreading with C++ 05 — Readers-Writers Problem

Complex use of std::unique_lock, std::mutex, & std::condition_variable

Yu-Cheng (Morton) Kuo
Nerd For Tech

--

Complete code: https://github.com/yu-cheng-kuo-28/multithreading-cpp

Compared to the producer-consumer problem, readers-writers problem offers flexibility for the readers to access the critical area at the same time, so we have to adopt semaphore to achieve that.

Figures: Illustrations of the readers-writers problem [1][2][3]

(1) Scenario 01: Readers first

Figure: The output of the code below

The output is not mixed here.

// 18_readers_writers_01.cpp
#include <iostream> // Includes standard I/O stream library
#include <thread> // Includes thread library for multi-threading
#include <mutex> // Includes mutex library for synchronization
#include <condition_variable> // Includes condition_variable library for thread waiting
#include <vector> // Includes vector library for dynamic array functionality

std::mutex resource_mutex; // Mutex for protecting shared resource access
std::mutex reader_count_mutex; // Mutex for protecting reader count variable
std::condition_variable cv; // Condition variable for thread synchronization
int reader_count = 0; // Global variable to keep track of reader count

void read(int reader_id) { // Function for reader thread
{
std::unique_lock<std::mutex> lock(reader_count_mutex); // Locks reader_count_mutex
reader_count++; // Increments reader count
if (reader_count == 1) {
resource_mutex.lock(); // Locks resource_mutex if first reader
}
}

std::cout << "Reader " << reader_id << " is reading." << std::endl; // Outputs reading status
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulates reading operation

{
std::unique_lock<std::mutex> lock(reader_count_mutex); // Re-locks reader_count_mutex
std::cout << "Reader " << reader_id << " has finished reading." << std::endl; // Outputs finished reading status
reader_count--; // Decrements reader count
if (reader_count == 0) {
resource_mutex.unlock(); // Unlocks resource_mutex if last reader
}
}
}

void write(int writer_id) { // Function for writer thread
resource_mutex.lock(); // Locks resource_mutex
std::cout << "Writer " << writer_id << " is writing." << std::endl; // Outputs writing status
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulates writing operation
std::cout << "Writer " << writer_id << " has finished writing." << std::endl; // Outputs finished writing status
resource_mutex.unlock(); // Unlocks resource_mutex
}

int main() { // Main function
std::vector<std::thread> readers; // Vector to store reader threads
std::vector<std::thread> writers; // Vector to store writer threads

for (int i = 1; i <= 5; ++i) { // Loops to create 5 readers and 5 writers
readers.push_back(std::thread(read, i)); // Starts reader thread
writers.push_back(std::thread(write, i)); // Starts writer thread
}

for (auto& t : readers) t.join(); // Waits for all reader threads to finish
for (auto& t : writers) t.join(); // Waits for all writer threads to finish

return 0; // Ends the program
}

Note the few points here:

  • [1] In this code, the mutex locks are used to manage access to a shared resource in a multi-threaded environment, particularly implementing the readers-writers problem.
  • [2] Resource Mutex (resource_mutex): This mutex is used to control access to the shared resource. Writers acquire this lock before they start writing and release it after they finish. This ensures that no other thread (reader or writer) can access the resource while writing is in progress.
  • [3] Reader Count Mutex (reader_count_mutex): This mutex controls access to the reader_count variable, which tracks the number of readers currently accessing the resource. When a reader starts, it increments reader_count, and when finished, it decrements it. The first reader to enter locks the resource_mutex to prevent writers from accessing the resource while readers are active. The last reader to exit unlocks resource_mutex, allowing writers to access the resource.
  • [4] Reader Function: Each reader locks reader_count_mutex, increments reader_count, and if it's the first reader, it also locks resource_mutex. After reading, it decrements reader_count, and if no more readers are left, it unlocks resource_mutex.
  • [5] Writer Function: Writers simply lock resource_mutex at the start of their operation and unlock it at the end. This direct use of resource_mutex ensures that writers have exclusive access to the resource.
  • [6] This mechanism allows multiple readers to read concurrently but ensures that writers have exclusive access, preventing simultaneous read-write and write-write operations.

In the code, the locking mechanisms for readers and writers serve different purposes:

  • [1] Readers (std::unique_lock<std::mutex>): The readers use std::unique_lock with reader_count_mutex to protect the reader_count. This lock allows multiple readers to increment and decrement the reader_count safely. std::unique_lock is more flexible and allows for lock management within a scope, which is necessary as readers only need to lock the resource_mutex when the first reader enters and unlock it when the last reader exits.
  • [2] Writers (resource_mutex.lock();): Writers directly use resource_mutex.lock() and resource_mutex.unlock() because they require exclusive access to the shared resource. There's no need for the same scoped lock management as in the readers' case, since writers hold the lock for the entire duration of their writing operation. Writers don't access the reader_count, hence they don't need the reader_count_mutex.
  • [3] The use of std::unique_lock in readers allows for more complex lock management within their function scope, handling the situation where multiple readers can access the shared resource concurrently, but ensuring mutual exclusion when a writer is operating. Writers, needing exclusive access, have a simpler lock mechanism as they don't share access with other threads during their operation.

Now, to make the output mixed, we will introduce delays between the readers and the writers in the next example.

(1) Scenario 02: Mixed

Figure: The output of the code below
// 19_readers_writers_02.cpp
#include <iostream> // Includes the standard I/O library
#include <thread> // Includes the thread library for using threads
#include <mutex> // Includes the mutex library for synchronization
#include <condition_variable> // Includes the condition_variable library for waiting for conditions
#include <vector> // Includes the vector library for using vector container
#include <random> // Includes the random library for generating random numbers
#include <chrono> // Includes the chrono library for dealing with durations

std::mutex resource_mutex; // Mutex to protect shared resource access
std::mutex reader_count_mutex; // Mutex to protect reader count
std::condition_variable reader_cv, writer_cv; // Condition variables for readers and writers
int reader_count = 0, writer_count = 0; // Counters for the number of readers and writers
bool writer_waiting = false; // Flag to indicate if a writer is waiting

std::random_device rd; // Random device to seed the generator
std::mt19937 gen(rd()); // Mersenne Twister generator
std::uniform_int_distribution<> dis(100, 500); // Uniform distribution for random sleep duration

void read(int reader_id) { // Function for reader threads
{
std::unique_lock<std::mutex> lock(reader_count_mutex); // Acquire lock for reader count
reader_cv.wait(lock, [] { return !writer_waiting; }); // Wait until no writer is waiting

reader_count++; // Increment reader count
if (reader_count == 1) {
resource_mutex.lock(); // Lock resource if first reader
}

std::cout << "Reader " << reader_id << " is reading." << std::endl; // Print reading message
}

std::this_thread::sleep_for(std::chrono::milliseconds(dis(gen))); // Simulate reading by sleeping

{
std::unique_lock<std::mutex> lock(reader_count_mutex); // Re-acquire lock for reader count
std::cout << "Reader " << reader_id << " has finished reading." << std::endl; // Print finished reading message
reader_count--; // Decrement reader count
if (reader_count == 0) {
resource_mutex.unlock(); // Unlock resource if last reader
writer_cv.notify_one(); // Notify one writer if any
}
}
}

void write(int writer_id) { // Function for writer threads
{
std::unique_lock<std::mutex> lock(reader_count_mutex); // Acquire lock for writer count
writer_count++; // Increment writer count
writer_waiting = true; // Set writer waiting flag

writer_cv.wait(lock, [] { return reader_count == 0; }); // Wait until no readers are present
resource_mutex.lock(); // Lock resource for writing

std::cout << "Writer " << writer_id << " is writing." << std::endl; // Print writing message
}

std::this_thread::sleep_for(std::chrono::milliseconds(dis(gen))); // Simulate writing by sleeping

{
std::unique_lock<std::mutex> lock(reader_count_mutex); // Re-acquire lock for writer count
std::cout << "Writer " << writer_id << " has finished writing." << std::endl; // Print finished writing message
writer_count--; // Decrement writer count
writer_waiting = writer_count > 0; // Update writer waiting flag
resource_mutex.unlock(); // Unlock resource after writing
if (writer_waiting) {
writer_cv.notify_one(); // Notify next writer if any
} else {
reader_cv.notify_all(); // Notify all readers if no writers
}
}
}

int main() { // Main function
std::vector<std::thread> readers; // Vector to store reader threads
std::vector<std::thread> writers; // Vector to store writer threads

for (int i = 1; i <= 5; ++i) { // Create and start 5 reader and writer threads
readers.push_back(std::thread(read, i)); // Start reader thread
writers.push_back(std::thread(write, i)); // Start writer thread
}

for (auto& t : readers) t.join(); // Wait for all reader threads to finish
for (auto& t : writers) t.join(); // Wait for all writer threads to finish

return 0; // End of program
}

Introduced the delays like “std::this_thread::sleep_for(std::chrono::milliseconds(dis(gen)));” to get a mixed output.

--

--

Yu-Cheng (Morton) Kuo
Nerd For Tech

CS/DS blog with C/C++/Embedded Systems/Python. Embedded Software Engineer. Email: yc.kuo.28@gmail.com