CIS 4307: Monitors

[Monitors], [Implementing Monitors]

Monitors

A Monitor is an abstract data type, i.e. a combination of data structures and operations, where at most one operation may be executing at one time. The required mutual exclusions are enforced by the compiler implementing the monitor.
Within a monitor it is possible to use a special kind of blocking semaphore, called a condition. Its operations are usually called wait for P, and signal for V. We will see later how conditions can be implemented. For now it suffices to know that the signal operation acts as a null operation in the case that no process is waiting on the condition.

Monitors were introduced to simplify the writing of concurrent programs since the direct use of spinlocks and semaphores results in programs that are hard to write correctly. Monitors are directly supported by languages such as Concurrent Pascal, Concurrent Euclid, Ada95. They are supported, in a simplified form, in Java[each object is a monitor with an implicit condition variable so that within the synchronized methods of the object one can use the methods wait, notify, and notifyAll]. Monitors, as a programming style, can be implemented in concurrent programs written in C using Unix system services with or even without threads [use processes, shared memory, and some form of semaphore].

Here is how we can write the Protected Bounded Buffer (it can be used to solve the Producers and Consumers problem) as a monitor:


Monitor PBBuffer
  b : BBuffer;  // This is an unprotected bounded buffer
  count : Integer;
  empty, full : condition;
procedure Init;
begin
  init(empty); init(full); init(b); count := 0;
end;
procedure Enqueue(I : Item)
begin
  if count == BufferSize(b) then wait(full);
  //BufferSize returns the maximum size of b
  count++;
  Enqueue(b, I);
  signal(empty);
end;
procedure Dequeue(I : out Item)
begin
  if count == 0 then wait(empty);
  count--;
  Dequeue(b, I);
  signal(full);
end;

Here is a monitor solution for the Dining Philosophers Problem [this solution is not fair]:

Monitor DiningPhilosophers
  HMNY  : constant integer = {the number of philosophers};
  HMNY1 : constant integer = HMNY - 1;
  state : array[0 .. HMNY1] of (thinking, hungry, eating) 
                            = (thinking, .., thinking);
  self  : array[0 .. HMNY] of condition; {We assume self's conditions 
                            are initialized}
function Left(K : 0 .. HMNY1) return 0 .. HMNY1 is
{This is an operation only used within the monitor}
begin
  return (K+1) mod HMNY;
end;
function Right(K : 0 .. HMNY1) return 0 .. HMNY1 is
{This is an operation only used within the monitor}
begin
  return (K-1) mod HMNY;
end;
procedure Test(K : 0 .. HMN1)
{This is an operation only used within the monitor}
begin
  if state[Left(K)] /= eating and
     state[K] == hungry and
     state[Right(K)] /= eating then {
     state[K] = eating;
     signal(self[K]); }
end;
procedure Pickup(I : 0 .. HMNY1)
begin
  state[I] = hungry;
  Test(I);
  if state[I] /= eating then wait(self[I]);
end;
procedure PutDown(I : 0 .. HMNY1)
begin
  state[I] = thinking;
  Test(Left(I));
  Test(Right(I));
end;

Each philosopher Pi will execute a simple loop:
  loop
    think;
    DiningPhilosophers.PickUp(i);
    eat;
    DiningPhilosophers.PutDown(i);
  forever;

Like most things also monitors are not perfect. For example it is easy to get into deadlocks if a monitor calls on another monitor. Suppose we have a PBBuffer and we write another monitor Moo with two operations Insert and Remove. Among other things, Insert calls on PBBuffer.Enqueue and Remove calls on PBBuffer.Dequeue. Now process A keeps on calling Moo.Insert until the buffer fills up. Then A calls Moo.Insert again. At this point A will be inside of Moo since the buffer is full. Another process B now calls Moo.Remove. It cannot enter Moo since A is inside of Moo. In turn A cannot progress until B (or somebody else, but we assume that nobody else is around) removes things from the buffer. Deadlock.

Implementing Monitors

Here we see the implementation of monitors using semaphores in the case that the Signal command can be applied anytime within the monitor, not just, as in the Brinch Hansen implementation for monitors, only when exiting monitor operations. It is a variation of the Hoare implementation and is close to the way that monitors behave in Posix threads.

Most of the code we present is modified from code in Silberschatz et al: Operating Systems Concepts.

We want to show how to implement a monitor using semaphores.

Notes

Within the monitor (intermediate) scheduling takes place. We are with three (semaphore) queues:

In this implementation the active process is given precedence over the processes that are coming out of the condition queue; processes in the next queue take precedence over processes in the mutex queue [this is a form of FIFO scheduling].