Mutexes and condition variables: protect a predicate
Design a blocking queue around protected state, repeated predicate checks, orderly shutdown, and an explicit lock-order argument.
On this page
The question for today
What you’ll understand
- State the invariant protected by a mutex.
- Explain why condition-variable waits recheck a predicate after reacquiring the lock.
- Audit notification, shutdown, and lock ordering without treating notifications as stored messages.
A condition variable is useful when a worker should sleep until shared state permits work. The essential object is the protected predicate, such as a queue containing an item or a shutdown flag becoming true. Notifications help waiting threads reconsider that predicate; they do not replace it. This lesson develops a bounded work-queue design in C++ with one mutex and separates correct state transitions from capacity and fairness questions that require their own evidence.
Make the protected state explicit
Suppose one mutex guards a queue, its item count, its capacity, and a closed flag. Every access that can conflict uses this same protection, including checks that appear harmless. A producer may append only when capacity remains and closure has not occurred. A consumer may remove only when an item exists. Describe what closed means: no future appends, while already queued items may still drain. Keeping related fields under one lock makes this multi-field invariant understandable.
Acquiring a mutex excludes other exclusive owners, and the synchronization rules connect prior protected writes to subsequent owners. Scope-based lock management helps release ownership on ordinary returns and exceptions. It does not prevent all design errors: callbacks invoked inside the critical section might acquire another lock, block on a network request, or reenter the same object. Keep the protected transition small, and decide explicitly whether moving or destroying an item may execute arbitrary application code.
Wait releases the lock and later reacquires it
A consumer acquires the mutex, checks the predicate, and waits when neither work nor closure permits progress. A condition-variable wait atomically unlocks and blocks as specified by the API, then reacquires the mutex before returning. Use a loop or the predicate overload. Waking does not reserve an item: another consumer may acquire the lock first, and spurious wakeups are allowed. The predicate must be evaluated while its associated shared state is properly protected.
A producer changes the guarded state, then notifies an appropriate waiter. Notification may occur while holding the lock or after releasing it when the surrounding lifetime and protocol are correct. The important handoff is the protected state transition and subsequent lock acquisition, not a belief that a notification accumulates as a future permit. On shutdown, update closed under the mutex and wake all classes of waiters whose predicates can now change. A bounded producer may need its own not-full condition and a closed-aware predicate.
Safety is separate from deadlock and throughput
A perfectly protected queue can still deadlock if consumers hold its mutex while waiting on a second resource needed by producers. Establish one lock order or use a suitable multi-lock operation where the contract fits; draw a wait-for graph when analyzing a failure. Do not assume a normal mutex or notify-one policy grants starvation freedom. Scheduling, critical-section duration, task behavior, and implementation details influence latency even when all state accesses are race-free.
Use the queue lab for capacity intuition only. For a growing fixed-rate run, choose Reset, explicitly set arrival to four and service to three items per second, predict the slope, then Play or Step. For a separate draining run, choose Reset again, explicitly set both rates to arrival two and service three, predict, then Play or Step from the initial four jobs. Changing a rate pauses playback but retains elapsed time and recomputes from the initial backlog; it does not preserve a prior backlog across a live rate switch. The fluid backlog is continuous and unbounded above, so it does not implement the bounded queue, notifications, a mutex, or wakeup scheduling. Its separate M/M/1 residence estimate assumes a stable stochastic model. Compare the lab's capacity warning with a real design's explicit blocking, rejection, and shutdown behavior rather than mistaking it for a correctness test.
Work it through
Drain a closed queue without losing the final items
A queue has capacity eight, contains three items, and has two consumers. A producer appends two items before closing. Assume successful protected operations and no later producers. Track the legal state changes.
The producer takes the mutex and appends two items: q changes from three to five, within capacity eight. It publishes closure under that mutex and notifies all consumers so an empty-queue waiter can eventually observe closure.
Consumers repeatedly reacquire the mutex, test the predicate, and remove one item when q is positive. Five successful removals change q through four, three, two, one, and zero; the distribution between workers is not specified.
Once a consumer observes closed and q equal to zero under the lock, it exits. Join both consumers before destroying the queue, mutex, or condition variables. A notification is not counted as an extra item or a guaranteed scheduled turn.
Exactly five queued items remain eligible to drain under this shutdown contract. Safety does not determine which consumer gets each item or when the scheduler runs it.
The living notebook
Explore the model
Interactive lab · queue
Capacity pressure before synchronization policy
Compare separate fixed-rate runs: Reset, explicitly set both arrival λ and service μ, predict, then Play or Step. Use λ=4, μ=3 for growth and Reset plus λ=2, μ=3 for draining from four items. A rate edit retains elapsed t and recomputes the model; it is not a live backlog-preserving switch. This model has no locks, condition variables, or bounded capacity.
- Fluid backlog
- 4
- Net rate · λ − μ
- −1 /s
- M/M/1 mean residence
- 1 s
Fluid backlog starts at 4 and follows max(0, 4 + (λ − μ)t). The separate M/M/1 residence-time formula assumes Poisson arrivals, exponential service and λ < μ. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forWhen arrivals exceed service, a bounded implementation needs an explicit response even if its locking is correct.
Pause & reason
Check your understanding
Your turn
Try it yourself
A capacity-four queue is full and two producers wait for space. The owner closes it without removing items. What shared transition and notifications are needed, and what must a woken producer do?
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
A full queue may never become non-full after closure. Include closure in the producer's wake predicate and distinguish waking from acceptance.
Compare with the solution
Set closed under the queue mutex, then notify all producer waiters as well as any consumers whose closure predicate matters. Each producer wakes, reacquires the mutex, and observes closed. It rejects or returns its not-yet-accepted item according to the API; it must not enqueue merely because the wait ended. Remaining items can drain if the contract allows it. Join all users before destroying synchronization state. Notify-one alone could leave the second producer asleep indefinitely after final shutdown.
Keep the idea
Take it with you
- Protect a state invariant, including predicate reads, with a clear locking discipline.
- A condition-variable wake prompts a guarded predicate check; it does not store an item permit.
- Shutdown, lock order, lifetime, fairness, and throughput need explicit arguments.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.