Lecture 9: Mesa style monitors

Reader/writer locks

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:

reader-writer overview
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.

Steps to implement reader/writer monitor

  1. Write down list of methods and their specs

    • enter_reader:
      • block until there are no writers,
      • record that this thread is reading
    • leave_reader:
      • remove current reader thread
    • enter_writer:
      • block until there are no readers or writers
      • record that this thread is a writer
    • leave_writer:
      • remove current writer thread
  2. 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

  3. 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
  4. 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()
  5. 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()
  6. 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.

Building a monitor in general

Overview

Rule 1: Global monitor lock

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

Python uses this latter approach, and provides Lock objects for this purpose:

Python monitor implementation
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.

Python monitor using with
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.

Rule 2: Condition variables and predicates

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:

No writers condition
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:

Additional predicates
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.

Rule 3: Implementing the monitor methods

Monitor methods should only do a few things: - read and update internal state (fields) of the monitor - wait for a predicate to become true

state updates

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.

Notification example
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()

waiting for predicates

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:

Hoare-style waiting: DO NOT WRITE THIS CODE
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:

MESA-style waiting: WRITE THIS CODE
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

Monitor implementation

The monitor implementation is very similar to the semaphore implementation, so we did not spend too much time on it. Here is the summary:

For the curious, here is a pseudocode implementation of Python's Lock and Condition classes.