Lecture 8: Reader/writer lock monitor

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