reader/writer step-by-step
3-way join problem
A notification causes a waiting thread to wake up, recheck the corresponding predicate, and (if the predicate no longer holds) go back to sleep. A call to notify is always safe (because you always recheck the predicate after wait returns) but is inefficient if the predicate is not true when the waiting thread wakes up.
We can avoid these inefficiencies in two ways:
notifyAll is always called when a predicate might become true because of a state update. To avoid spurious wakeups, you should ensure that the predicate did not hold before the wakeup and does hold afterwards.
You can use preconditions and monitor invariants to guarantee this, but if you do, be sure to document them clearly!
For example, in the enter_read method of a reader/writer lock monitor, we increment the number of readers. Because we know that the number of readers is always non-negative, this cannot possibly cause the number of readers to become zero, so we do not have to notify the condition variable for a "readers == 0" predicate.
notifyAll wakes every thread that is waiting for a predicate. If you can guarantee that any thread waiting for predicate will invalidate the predicate, then you can call notify instead of notifyAll, which only awakens a single waiting thread, instead of all of them.
For example, reader/writer lock implementation (details below) has a condition variable no_writers
with the predicate not is_writing
. The only method that waits on this condition is the enter_writer
method; and this method is guaranteed to set is_writing
to true after waiting on no_writers
. Therefore, it is safe to call notify instead of notifyAll
Both of these methods are dangerous, because they require careful global reasoning about the entire monitor instead of local reasining about an individual method. For example, you might think that an enqueue method can call notify instead of notifyAll, since any waiting thread that wakes up will dequeue the enqueued element. However, this can lead to deadlock in the following situation:
These techniques do not improve safety or liveness: adding extra notifications cannot break a working monitor. However, they can improve efficiency, so they should be considered: if it is clear why the code remains correct, consider replacing a notifyAll with a notify or removing an unnecessary notifyAll.
Write down list of methods and their specs
Pick a method and implement it: enter_reader
always always always acquire monitor lock
def enter_reader(self):
with self.lock:
need to block until there are no writers. Add a no_writers
condition variable. Write down the predicate, which requires a new field:
def __init__(self):
self.lock = Lock()
self.is_writer = False
self.no_writers = Condition(self.lock)
# predicate: not self.is_writer
def enter_reader(self):
with self.lock:
while not (not self.is_writer):
self.no_writers.wait()
Note: could replace not (not self.is_writer)
with is_writer
. However, this makes it less clear that the while loop guard is a copy of the predicate. Simplification is a matter of preference, but I prefer not simplifying in this case.
method currently complete, because it updates all the relevant variables we've written down so far and ensures its postcondition
Choose another method and implement it: enter_writer
always need to acquire monitor lock
def enter_writer(self):
with self.lock:
wait until no readers or writers. This is a new predicate, so we need new condition variables, and state
def __init__(self):
self.lock = Lock()
self.is_writer = False
self.num_readers = 0
self.no_writers = Condition(self.lock)
# predicate: not self.is_writer
self.no_rw = Condition(self.lock)
# predicate: self.num_readers == 0 and not self.is_writer
...
def enter_writer(self):
with self.lock:
while not (self.num_readers == 0 and not self.is_writer):
self.no_rw.wait()
update state: there is now a writer so we must set is_writer to true. This does not make any predicates true, so no need to notify
def enter_writer(self):
with self.lock:
while not (self.num_readers == 0 and not self.is_writer):
self.no_rw.wait()
self.is_writer = True
# no need to notify
method is currently complete because it correctly updates state and ensures its postcondition
We've added new state and condition variables, so need to revisit enter_reader. enter_reader should update the number of readers before returning, but no reason to notify (since incrementing num_readers can't make num_readers == 0)
def enter_reader(self):
with self.lock:
while not (not is_writer):
self.no_writers.wait()
self.num_readers++
# no need to notify
implement leave_reader:
always always always grab monitor lock:
def leave_reader(self): with self.lock:
leave reader doesn't need to block (this isn't hotel california: you can leave whenever you want)
but it does update state.
def leave_reader(self): with self.lock: self.num_readers -= 1
this may have made the no_rw predicate true, so notify:
def leave_reader(self): with self.lock: self.num_readers -= 1 self.no_rw.notifyAll()
method is currently complete because it correctly updates state and ensures its postcondition
Optional: we can optimize slightly: we only make no_rw true if we've decremented to 0. Moreover, by examining the entire code, we see that no_rw always makes invariant false after wait returns, so we can notify one instead of notifyAll.
def leave_reader(self):
# WARNING: optimized code needs to be reexamined if this class changes
with self.lock:
self.num_readers -= 1
if self.num_readers == 0:
self.no_rw.notify()
The last method: leave_writer
always grab monitor lock
def leave_writer(self):
with self.lock:
no waiting. Updates is_writer
def leave_writer(self):
with self.lock:
self.is_writer = False
this may make either invariant true, so we must notify both
def leave_writer(self):
with self.lock:
self.is_writer = False
self.no_rw.notifyAll()
self.no_writers.notifyAll()
method is currently complete because it correctly updates state and ensures its postcondition
optional: optimize. We are guaranteed to change no_writers from false to true. Moreover, since we guarantee that whenever is_writer is true, num_readers is 0, we are guaranteed to change no_rw from false to true. However, we can change no_rw.notifyAll to no_rw.notify since the only wait is guaranteed to invalidate the predicate
def leave_writer(self):
# warning: optimized code, needs to be reexamined if class changes
with self.lock:
self.is_writer = False
self.no_rw.notify()
self.no_writers.notifyAll()
Ship the full implementation. We could do some testing, but testing gives no guarantees because of non-determinism. We must draw our confidence from sticking to the monitor design pattern, and reasoning about pre- and post-conditions.
We began this process with the three-way join problem, defined as follows: there are three kinds of threads: red, green, and blue. To start their work, the threads call enter_red
, enter_green
, or enter_blue
respectively.
Threads should be allowed to proceed in groups of three, one of each color. For example, a thread calling enter_red should block until there is also a red and a blue thread, and at that time all threads should be released simultaneously.
We will discuss the solution tomorrow.