This is a sample program meant to illustrate the pattern of reader-writer lock. Most demonstrations of the reader-writer lock start some readers and writers and let them sleep for some fixed amount of time to simulate the time during which they are reading or writing. This doesn't really allow one to effectively test their hypotheses and understand the pattern. To overcome this limitation, this program works a bit differently. You control when to start a reader or writer and when to exit a reader or writer. In this way you basically control the reader-writer lock. This is done using following commands (key presses):
r
will start a readerw
will start a writerx
will exit a readerq
will exit a writer- any other key press will exit the program
As example, to verify that no reader can read while a writer is writing, I will press following sequence of keys:
w
to start a writer and verify it can writer
to start a reader and verify it cannot readq
to exit the writer and verify the reader can now readx
to exit the reader
Using above as guideline, verify following assertions:
- Assertion 1: No writer can start writing while a reader is reading. To verify this first start a reader (r) and then try to start a writer (w).
- Assertion 2: No writer can start writing while another writer is writing. To verify this first start a writer (w) and then try to start another writer (w).
- Assertion 3: No reader can start reading while a writer is writing. To verify this first start a writer (w) and then try to start another reader (r).
- Assertion 4: Multiple readers can read concurrently at the same time provided no writer is writing. To verify this first start a reader (r) and then try to start another reader (r).
In other words an X-lock will block. The table below shows two threads and the kind of lock they have acquired (or are attempting to acquire) on a shared mutex. S stands for shared lock and X stands for exclusive lock.
Thread 1 | Thread 2 | Blocks |
---|---|---|
S | X | Yes |
X | X | Yes |
X | S | Yes |
S | S | No |
./compile.sh
./main
The most important piece of code to understand in this program is this:
cv.wait(lock, [this] { return is_finished; });
What does it do? let's break it down into step-by-step:
[this] { return is_finished; }
defines a lambda function in C++. The arguments inside[...]
become part of the closure variables.- The lambda function is evaluated.
- If the lambda function returns
true
, the thread will block until it has acquired thelock
. - If the lambda function returns
false
, the thread will release thelock
(only if it already has it), and will enter sleep state until its woken up by another thread via a call tonotify
ornotify_one
. - That's it. When the thread wakes up, all the steps starting from 1 will be repeated again.
The point to note here is that in step 3, it is entirely possible that the lambda function returns true, the thread blocks until it has acquired the lock
but by the time it has acquired the lock and ready to execute the next piece of code, the condition in the lambda function might no longer be true.
Because of this, in some places I have seen a while
loop being used to recheck if the condition is still true (e.g. refer the section titled
Monitor usage on this page; I do not do this in my code btw):
std::unique_lock<std::mutex> lock {mx}; // acquire the lock
while (!condition) { // if condition is already true we don't need to enter the loop
cv.wait(lock, [this] { return condition; });
}
To finish off, lets also cover what notify
and notify_one
do. When a thread calls notify
, it will wake up all other threads waiting
on the CV (condition variable) whereas notify_one
will wake up only one thread (the thread to wake up is picked at random amongst the pool of waiting
threads). waiting = sleeping.