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
signalon 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 releaselock, 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.