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
signalneeded, 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 tomis 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 ifcondholds. _Elseclause, 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.