MESA-style monitors (in python and java)
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, or see the little book of semaphores ). 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.
in monitor initialization, create a single lock; every monitor method must acquire the lock before running and release it before returning
while not [predicate]: [condition].wait()
monitor methods may read and update state freely, but if an update would cause a predicate to become true, it must call notifyAll on the corresponding condition variable.
This rule means that we can freely read and update fields of the monitor without worrying about race conditions. Monitor library implementations typically ensure this in one of two ways:
Java's synchronized methods are an example of this approach: the Java runtime uses locks to ensure that two synchronized methods of the same object cannot run simultaneously
library support and programming idioms: languages without baked-in support for monitors require the programmer to explicitly acquire and release a monitor lock; the programmer must acquire the lock at the beginning of every method and release it before the method returns.
There is only one lock per monitor; by convention this lock is always called "lock".
Python uses this latter approach, and provides Lock objects for this purpose:
class MyMonitor(object): def __init__(self): self.lock = Lock() def monitor_method_2(self): self.lock.acquire() # ... monitor method implementation ... self.lock.release() def monitor_method_1(self): self.lock.acquire() # ... monitor method implementation ... self.lock.release() |
Python provides special "with" syntax that can be used in place of explicit calls to acquire and release. Code within a "with lock:" block will acquire the lock before it begins and release it before it ends.
class MyMonitor(object): def __init__(self): self.lock = Lock() def monitor_method_2(self): with self.lock: # ... monitor method implementation ... def monitor_method_1(self): with self.lock: # ... monitor method implementation ... |
With clauses are preferred to explicit acquire and release calls, because they guarantee that the lock is freed no matter how the code exits; it can be easy to forget to release the lock if, for example, the monitor code throws an exception or returns in multiple places.
In the reader writer example, the ready_to_read function must wait until there are no writers (it isn't safe for a reader to proceed if there are writers). This invariant would need a corresponding condition variable in the monitor, as well as enough state to determine whether the invariant is true or false:
class ReaderWriterMonitor(object): def __init__(self): self.lock = Lock() self.num_writers = 0 # predicate: self.num_writers == 0 self.no_writers = Condition(self.lock) |
We may also want a condition indicating that there are no readers. Perhaps we'll find we also want to block until there are no threads whatsoever. Each new predicate should have a corresponding condition variable:
class AnotherMonitor(object): def __init__(self): self.lock = Lock() self.num_writers = 0 self.num_readers = 0 # predicate: self.num_writers == 0 self.no_writers = Condition(self.lock) # predicate: self.num_readers == 0 self.no_readers = Condition(self.lock) # predicate: self.num_writers == 0 and self.num_readers == 0 self.no_threads = Condition(self.lock) |
Language specific notes: python condition variables require a reference to the monitor lock. Java uses objects as both monitors and condition variables, which forces each monitor to have only one condition variable.
Important: Writing down the predicate is critical.
Monitor methods should only do a few things: - read and update internal state (fields) of the monitor - wait for a predicate to become true
It is not necessary to worry about race conditions or lost updates inside of a monitor method, because of rule 1.
However, any update that may cause one of the monitor's predicates to become true must call notifyAll on the corresponding condition variables.
class AnotherMonitor(object): def __init__(self): self.lock = Lock() self.a = 0 self.b = 0 self.c = 0 # predicate: a > b + c self.enough_as = Condition(self.lock) # predicate: a >= 0 self.a_nonneg = Condition(self.lock) def increment_a(self): with self.lock: self.a += 1 if self.a == self.b + self.c + 1: # enough_as predicate has just become true # we should tell the world self.enough_as.notifyAll() if self.a == 1: # a_nonneg predicate has just become true # we should tell the world self.a_nonneg.notifyAll() |
If a monitor method wants to ensure that some predicate holds, it must wait.
In the original design of monitors, (Hoare-style monitors), one might write code like the following:
class AnotherMonitor(object): def __init__(self): self.lock = Lock() def check_safe(self): wait until self.all_clear # idea: can assume self.all_clear is true at this point |
The intent is that if all is not clear, then the check_safe method should wait until it is safe, and then return.
However, it is very difficult to implement condition variables that ensure that the predicate actually holds when wait returns; we must prevent other threads from running between when the predicate becomes true and when wait returns.
For this reason, almost all monitor support libraries use MESA-style monitors. With MESA-style condition variables, the only guarantee that wait gives upon returning is that the corresponding predicate was true at some point. Users are responsible for rechecking the predicate after wait returns and waiting again if necessary.
In practice, this means that to wait for something to become true, you wait within a while loop:
class AnotherMonitor(object): def __init__(self): self.lock = Lock() # predicate: all_clear = True self.safe = Condition(self.lock) def check_safe(self): with self.lock: while not self.all_clear: self.safe.wait() # at this point we are guaranteed that self.all_clear is true |
The monitor implementation is very similar to the semaphore implementation, so we did not spend too much time on it. Here is the summary:
each monitor has a spin lock and a queue of waiting threads (in python, this is stored in the Lock object). Acquiring the lock while it is being held causes the thread to be descheduled and placed on the monitor lock's queue
each condition variable also has a queue of waiting threads. When a thread calls wait, the monitor lock is released, and the thread is descheduled and placed on the condition variable's waiting queue.
when notifyAll is called on a condition variable, all of the threads that were waiting on the condition variable are placed in the waiting queue for the lock (they must re-acquire the lock before they become runnable again).
For the curious, here is a pseudocode implementation of Python's Lock and Condition classes.