a condition variable
Sometimes a thread holds a lock but discovers it cannot continue until some condition becomes true — the queue it wants to read from is empty, the buffer it wants to fill is full. Busy-waiting on that condition while holding the lock would be doubly wrong: it wastes CPU, and it keeps the lock so the very thread that could make the condition true can never get in. A condition variable solves this. It is a waiting room, always paired with a mutex, that lets a thread atomically release the lock and go to sleep until another thread signals that the condition may now hold.
It has three operations. wait(cv, lock): the caller, which must already hold lock, atomically releases lock and blocks on cv; when later woken, it re-acquires lock before returning. The atomic release-and-sleep is the crucial trick — it closes the gap in which a wakeup could otherwise be missed. signal(cv): wakes one thread waiting on cv (if any). broadcast(cv): wakes all threads waiting on cv. The canonical pattern is: acquire the lock, then while (condition is false) wait(cv, lock); then act, then release. A thread on the other side changes the state, then signals or broadcasts so waiters re-check.
Two non-negotiable rules, both common sources of bugs. First, always re-check the condition in a while loop, never a single if. A thread can wake up for reasons other than your condition being true (another waiter got there first and consumed the resource; some systems allow spurious wakeups), so after waking it must re-test the condition and possibly wait again. Second, a condition variable is stateless and memoryless: a signal sent when no one is waiting is simply lost, not remembered. This is the opposite of a semaphore, whose count remembers signals. So the condition itself must live in the shared state guarded by the mutex; the condition variable only manages the sleeping and waking, never the truth of the condition.
A worker waits for jobs: acquire(lock); while (queue is empty) wait(cv, lock); job = dequeue(); release(lock). A submitter does: acquire(lock); enqueue(job); signal(cv); release(lock). The while (not if) re-check is essential: another worker may have grabbed the job before this one re-acquires the lock.
Always wait inside while(condition false); a signal with no waiter is lost, so the condition lives in shared state.
Never wait under an if — re-check with while, because of spurious wakeups and stolen wakeups. And remember a condition variable has no memory: a signal to no one is gone forever.