Monitors

Semaphore implementation

This has been added to the lecture 7 notes.

Monitors

Semaphores work well for the common design patterns (signalling, resource allocation, and mutual exclusion), but semaphore code becomes difficult to reason about as the size of the code or the complexity of the synchronization pattern grow.

To determine how the semaphore is supposed to work, you need to look at all of the code that calls P() or V(), and reason about the order in which those calls happen. It is hard to answer the question "what do I know about the state of the world" right after V returns.

Monitors are a design pattern that are much easier to reason about: they make the changes to the state and the conditions that you are waiting for much more explicit.

The initial design for monitors (Hoare style monitors) imagine that the programming language implementation provides primitives for mutual exclusion and blocking.

For example, the interface for a semaphore says that P waits until the counter is positive and then decrements it, while V increments the counter. Hoare imagined that the code for the Semaphore should read as follows:

class Semaphore(Monitor):
  def __init__(self,count):
    self.count = count
    # rep invariant: count >= 0

  def P(self):
    wait until self.count > 0
    self.count--

  def V(self):
    self.count++

By marking the class as a Monitor, the language would guarantee that no threads could call P and V at the same time, so there is no need to worry about lost updates to the count variable. The primitive "wait until" would guarantee that we don't decrement the count until we know it is positive.

As a second example, we might implement the bounded buffer exactly as we would write the pseudocode:

class BoundedBuffer(Monitor):
  def __init__(self,N)
    self.queue = new Object[N]
    self.N     = N
    self.in    = 0
    self.out   = 0
    # invariant:
    #    if in != out, then
    #       queue[out], queue[out+1], ..., queue[in-1] contain objects
    #       that have been put but not gotten; all other entries are None
    #    otherwise, all objects have been gotten, and all entries are None

  def put(self, obj):
    wait until self.in + 1 != self.out
    self.queue[in++] = obj

  def get(self):
    wait until self.in != self.out
    result = self.queue[out]
    self.queue[out] = None
    out++
    return result

#NOTE: all operations on in and out should be (mod N)

Again, the code describes exactly what it does. It is obviously safe: we know that the queue is non-empty when we get, because we wait for it. We know that the queue is non-full when we put, because we wait for it. We could easily write a CS2110-style proof of correctness of this code.

In reality, most languages do not provide Hoare-style monitors, but they can easily be simulated using lock and condition variable objects. This approach is called a MESA-style monitor (named after the MESA operating system, which first implemented them); we will discuss them in the next lecture.