Lecture 9: Practice with monitors

Reducing spurious wakeups

A notification causes a waiting thread to wake up, recheck the corresponding predicate, and (if the predicate no longer holds) go back to sleep. A call to notify is always safe (because you always recheck the predicate after wait returns) but is inefficient if the predicate is not true when the waiting thread wakes up.

We can avoid these inefficiencies in two ways:

Both of these methods are dangerous, because they require careful global reasoning about the entire monitor instead of local reasining about an individual method. For example, you might think that an enqueue method can call notify instead of notifyAll, since any waiting thread that wakes up will dequeue the enqueued element. However, this can lead to deadlock in the following situation:

These techniques do not improve safety or liveness: adding extra notifications cannot break a working monitor. However, they can improve efficiency, so they should be considered: if it is clear why the code remains correct, consider replacing a notifyAll with a notify or removing an unnecessary notifyAll.

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.

Three-way join

We began this process with the three-way join problem, defined as follows: there are three kinds of threads: red, green, and blue. To start their work, the threads call enter_red, enter_green, or enter_blue respectively.

Threads should be allowed to proceed in groups of three, one of each color. For example, a thread calling enter_red should block until there is also a red and a blue thread, and at that time all threads should be released simultaneously.

We will discuss the solution tomorrow.