Lecture 7: Monitors

Synchronization problems

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.

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