Monitor (OS)

Monitor is a software module that uses condition variables for signalling.

  • Unused signals are lost (!= semaphores)

Chief characteristics

  • Local data variables are accessible only by the monitor (⇒ shared data in monitor is “safe”)
  • Process enters monitor by invoking one of its procedures (⇒ controlled entry)
  • Only one process may be executing in the monitor at a time (⇒ mutex)

uC++ _Monitor (CS343)

uC++ provides a monitor keyword, _Monitor, that combines shared data with implicit mutual exclusion on its public members. Each monitor has an internal mutex lock; member entry Ps it, exit Vs it.

_Monitor AtomicCounter {
    int counter;                   // shared data
public:
    AtomicCounter( int init = 0 ) : counter( init ) {}
    int inc() { counter += 1; return counter; }  // implicit mutex member
    int dec() { counter -= 1; return counter; }
};
AtomicCounter a, b{1}, c{3};       // accessed by multiple threads
  • _Mutex qualifier, explicitly mark a non-public member as mutex
  • _Nomutex qualifier, remove implicit mutex (e.g., const getters that don’t need serialization)
  • Recursive entry allowed, one mutex member can call another (or itself) on the same owner
  • Destructor is always mutex, ending a scope with a monitor, or deleteing one, blocks until the thread leaves
  • Unhandled exceptions release the lock, the monitor remains usable after an exception propagates out

How _Monitor is implemented (Buhr §8.2)

Every _Monitor is compiled to a plain class with one owner mutex lock and three runtime-managed queues. Each public (mutex) member is wrapped in implicit acquire / release:

class Mon {                              // what _Monitor expands to
    MutexLock mlock;                     // implicit monitor lock
    int v;                               // shared data
  public:
    int x( ... ) {                       // mutex member
        mlock.acquire();                 // P on entry (may block in calling queue)
        // ... body ...
        mlock.release();                 // V on exit
        return v;
    }
};

Three queues the runtime maintains for every monitor:

  • Calling (C), tasks that reached acquire but the owner lock was held
  • Signalled (W), tasks woken by uCondition::signal, waiting to re-enter
  • Signaller (S) / acceptor stack, tasks that called signal / _Accept and are themselves suspended until their action completes

The relative priority among C/W/S is the single design choice that decides barging, starvation, and Hoare-vs-Mesa semantics, see Monitor Types for the full table.

_Accept is scheduling on the queue set

_Accept( member ) pushes the current task onto the acceptor stack and tells the scheduler “release the monitor lock, but only accept a call to member next, not any arrival in C.” When the accepted call exits (or waits on a condition), the acceptor is resumed. Nested _Accepts form the stack of blocked acceptors, each waits for its callee to exit before the one below it resumes.

uCondition is a plain waiter queue, not kernel-managed

A uCondition is just a queue of uBaseTask *. wait() atomically enqueues the current task and releases mlock (via yieldNoSchedule, the same primitive used by baton passing); signal() dequeues a waiter and moves it onto the signalled queue W. No kernel calls, all of this is coroutine-style context switching inside the uC++ runtime.

Scheduling inside a monitor

Two techniques:

Classification by queue priority (C/W/S) ⇒ see Monitor Types.