Monitor

External Scheduling

External scheduling controls which mutex member of a monitor or task may accept the next call, from the outside. In uC++ it’s spelled _Accept. Rather than waiters blocking on an internal condition variable, the monitor itself says “I’ll only accept a call to remove right now”, callers to other members wait in their mutex-member queues.

Why external scheduling instead of condition variables?

Because it’s simpler and barging-free by construction. No explicit signal needed, the accept is the signal. The control flow reads top-down: “if empty, don’t accept remove; if full, don’t accept insert.” Trade-off: external can’t condition on member arguments (e.g., match a key); for that, fall back to internal scheduling.

Canonical bounded buffer (external)

_Monitor BoundedBuffer {
    int front = 0, back = 0, count = 0;
    int elements[20];
public:
    _Nomutex int query() const { return count; }
    [_Mutex] void insert( int elem );
    [_Mutex] int  remove();
};
void BoundedBuffer::insert( int elem ) {
    if ( count == 20 ) _Accept( remove );     // block until a consumer removes
    elements[back] = elem; back = (back + 1) % 20; count += 1;
}
int BoundedBuffer::remove() {
    if ( count == 0 ) _Accept( insert );      // block until a producer inserts
    int elem = elements[front]; front = (front + 1) % 20; count -= 1;
    return elem;
}

Semantics

  • _Accept( m ), block until a call to m is outstanding; run that call’s body, then resume after the _Accept.
  • Alternative list, _Accept( m1 || m2 ) accepts whichever arrives first.
  • Conditional _When, _When( cond ) _Accept( m ) only accepts if cond holds.
  • _Else clause, terminating else runs if no accept matches immediately (tryacquire).
  • Accepted call runs like a normal member call; acceptor blocks on the A/S stack; when accepted call exits or waits, acceptor resumes.

When not usable

  • Scheduling depends on member argument values (e.g., dating service: match on compatibility code).
  • Acceptor must block in the monitor and can’t guarantee the next call satisfies cooperation.

In those cases use internal scheduling.

Task-main form (Buhr §9.2.1)

For a _Task, accepts typically live in main() rather than the member bodies, because the task has its own thread that loops accepting work:

_Task BoundedBuffer {
    int front = 0, back = 0, count = 0;
    int Elements[20];
  public:
    _Nomutex int query() const { return count; }
    void insert( int elem ) { Elements[back] = elem; }
    int  remove()            { return Elements[front]; }
  private:
    void main() {
        for ( ;; ) {
            _When( count != 20 ) _Accept( insert ) {            // after-call block
                back = (back + 1) % 20; count += 1;
            } or _When( count != 0 ) _Accept( remove ) {
                front = (front + 1) % 20; count -= 1;
            }
        }
    }
};

The after-call block runs in the task’s thread right after the member call returns to it, so state updates happen without re-entering the mutex. Order of _Accept clauses sets relative priority when multiple calls are outstanding.

2^N−1 if-statement equivalence

_When guards a given accept so only valid members are offered. With N members the full boolean expansion needs 2^N−1 if-statements:

if      ( C1 && C2 && C3 ) _Accept( M1 || M2 || M3 );
else if ( C1 && C2       ) _Accept( M1 || M2 );
else if ( C1 && C3       ) _Accept( M1 || M3 );
else if ( C2 && C3       ) _Accept( M2 || M3 );
else if ( C1             ) _Accept( M1 );
else if ( C2             ) _Accept( M2 );
else if ( C3             ) _Accept( M3 );

Each row must offer only the currently valid members, offering M1 when C1 is false would violate cooperation. _When + or collapses the pyramid into one statement: _When(C1) _Accept(M1) or _When(C2) _Accept(M2) or _When(C3) _Accept(M3).

Accept statement selection

  • Several accepts + multiple outstanding calls ⇒ pick by order of _Accept (earlier = higher priority).
  • All conditionals false ⇒ statement does nothing (like empty switch).
  • Some true but no calls ⇒ acceptor blocks until a matching call arrives.
  • Acceptor pushed on A/S stack (C < W < S priority: calling < waiting < signalled).
  • Terminating _Else { ... } clause ⇒ non-blocking try-accept.