Consider a shared variable that is read often but updated rarely. Want to allow many threads to read at the same time (but not while threads are writing), or allow a single thread to write (but not while threads are reading).
We might solve this by writing functions ready_to_read and ready_to_write which only return when it is safe to read or write, and done_reading and done_writing which update the synchronization state to allow other threads to proceed:
Shared state:
x : variable ... other synchronization state ... |
|
Code to read:
ready_to_read() # at this point we are # sure no writers are running y = x done_reading() |
Code to write:
ready_to_write() # at this point we are # sure no other writers # or readers are running x++ done_writing() |
It is possible but difficult to implement the ready_ and done_ functions using semaphores, and the solution is complicated and can be hard to understand (I encourage you to try this yourself). Monitors are an alternative design pattern that make the implementations of these complex interactions clearer.
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.