Lecture 8: Monitors

Synchronization problems

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). Monitors are an alternative design pattern that make the implementations of these complex interactions clearer.

Monitors

A monitor is an object (in the object-oriented programming sense): it has some internal state (fields) and some methods. Monitors are responsible for managing the interaction of threads: threads that need to interact will call methods on the monitor, and these methods will only return when it is safe for the threads to proceed.

Outline of reader/writer lock monitor
class ReaderWriterMonitor(object):
  def __init__(self):
    # initialize the monitor

  def ready_to_read(self):
    # block until it is safe to read
    #
    # postcondition: it is safe to read
    #                i.e. no threads are writing

  def done_reading(self):
    # update the monitor to indicate
    # that a thread is no longer reading
    #
    # precondition: current thread has
    #               called ready_to_read

  def ready_to_write(self):
    # block until it is safe to write
    #
    # postcondition: it is safe to write
    #    i.e. no threads are reading or writing

  def done_writing(self):
    # update the monitor to indicate
    # that a thread is no longer writing
    #
    # precondition: current thread has
    #               called ready_to_write

How to build a monitor

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

    # predicate: all_clear = True
    self.safe = Condition(self.lock)

  def check_safe(self):
    if not self.all_clear:
      self.safe.wait()

    # 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):
    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.

Monitors as state machines

I find it useful to think of a monitor as a state machine (like finite automata). The fields of the monitor tell you which state the machine is currently in. Predicates (condition variables) are the collection of states from which a method is allowed to make progress (much like accepting states). Methods cause the machine to transition. NotifyAll is called when the machine enters a state the satisfies the predicates.