[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.
P(mutex);
if next_count > 0 then V(next) else V(mutex);
Abstract Data Type condition is count : integer initialized to 0; queue : semaphore initialized to 0 (i.e. blocking); procedure wait (x : in out condition) is begin x.count++; if next_count > 0 then V(next) else V(mutex); P(x.queue); P(next); next_count--; end; procedure signal (x : in out condition) is begin if x.count > 0 then begin x.count--; next_count++; V(x.queue); end; end;
Within the monitor (intermediate) scheduling takes place. We are with three (semaphore) queues: