Mutual Exclusion

uCondLock

uCondLock is uC++‘s condition variable attached to a mutex lock (uOwnerLock). It is the classic wait/signal primitive: a task waits atomically releasing the lock, and another task signals to wake one waiter (or broadcast to wake all). Lock re-acquisition happens implicitly before wait returns.

Why not just a semaphore?

Because a condition variable has no internal counter, a signal on an empty queue is a no-op, not a remembered state. That matches the “wait until the world is in the right state” idiom: you re-check the predicate under the lock every time you wake (while (!pred) cond.wait(lock);). Semaphores conflate “counter” with “wait list”; conditions separate them.

Shape

uOwnerLock m;
uCondLock notFull, notEmpty;
 
// producer
m.acquire();
while ( buf.full() ) notFull.wait( m );      // atomically releases m, blocks
buf.push( item );
notEmpty.signal();
m.release();
 
// consumer
m.acquire();
while ( buf.empty() ) notEmpty.wait( m );
T item = buf.pop();
notFull.signal();
m.release();

Interface

  • wait( lock ), atomically release lock, block; re-acquire before returning.
  • signal(), wake one waiter (FIFO). No effect if none.
  • broadcast(), wake all waiters.
  • empty(), true if no waiters.

Must re-check under the lock

A signal guarantees a waiter wakes, not that the predicate still holds, a third task could have raced in. Always loop on the predicate (while, not if).

CS343 canonical pattern, prevent lost signal (Buhr §6.3.2.3)

The classic T1-waits / T2-signals skeleton. T1 must acquire mlk before checking done so that T2 can’t signal into the gap between T1’s check and its wait.

bool done = false;                          // shared synchronization flag
uOwnerLock mlk; uCondLock clk;
 
_Task T1 {                                  _Task T2 {
    uOwnerLock & mlk;                           uOwnerLock & mlk;
    uCondLock  & clk;                           uCondLock  & clk;
    void main() {                               void main() {
        mlk.acquire();  // prevent lost signal      S1;
        if ( ! done )   // signal occurred?         mlk.acquire();   // prevent lost signal
            clk.wait( mlk );  // atomic release     done = true;     // remember it occurred
        mlk.release();  // release either way       clk.signal();    // signal lost if no waiter
        S2;                                         mlk.release();
    }                                           }
  public:                                     public:
    T1( uOwnerLock & m, uCondLock & c )         T2( uOwnerLock & m, uCondLock & c )
        : mlk(m), clk(c) {}                         : mlk(m), clk(c) {}
};                                          };
 
int main() {
    uOwnerLock mlk; uCondLock clk;
    T1 t1( mlk, clk ); T2 t2( mlk, clk );
}

Why the if (!done) guard matters: if T2 ran S1, acquired mlk, set done, signaled (lost, no waiter), and released before T1 ever got its acquire, then T1 starts, sees done==true, and skips wait entirely. Without the flag, T1 would block forever on a signal that already fired.

Synchronization locks are weaker than semaphores, a semaphore remembers Vs; a condition lock doesn’t. The done flag is what restores that memory.