Locks give mutual exclusion mutual exclusion The guarantee that if one thread is executing a critical section, others are prevented from doing so โ the property locks exist to provide, and the road back to deterministic output. defined in ch. 26 โ open in glossary , but not waiting. Yet threads constantly need to wait for something: a parent waiting for a child to finish (a join() join pthread_join(thread, &retval): block until the named thread completes, optionally collecting its void* return value. Long-lived servers may never join; parallel task programs join to know the work is done. (create-then-immediately-join has a simpler name: a procedure call.) defined in ch. 27 โ open in glossary ), a consumer waiting for data to arrive. How should that wait be built?
void *child(void *arg) {
printf("child\n");
// XXX how to indicate we are done?
return NULL;
}
int main(int argc, char *argv[]) {
printf("parent: begin\n");
pthread_t c;
Pthread_create(&c, NULL, child, NULL);
// XXX how to wait for child?
printf("parent: end\n");
}
The desired output is parent: begin / child / parent: end. The
obvious hack is a shared flag the parent spins on:
volatile int done = 0;
void *child(void *arg) { printf("child\n"); done = 1; return NULL; }
// parent:
while (done == 0)
; // spin โ works, but burns the CPU the whole wait
It works, but the parent wastes CPU spinning. Weโd rather put the parent to sleep until the condition comes true, and wake it precisely then.
The Crux: How To Wait For A Condition
How should a thread wait for a condition to become true โ without the gross inefficiency (and occasional incorrectness) of spinning?30.1 Definition and Routines
The answer is the condition variable condition variable The signaling primitive (pthread_cond_t): wait(cond, mutex) sleeps the caller โ releasing the mutex while asleep, reacquiring before return โ until another thread signal()s. Always called with the lock held; always re-checked in a while loop. The deep treatment arrives with ch30.
defined in ch. 27 โ open in glossary
(CV): an explicit queue threads put themselves on when some state
isnโt as desired (by waiting), and that another thread pulls from when
it changes that state (by signaling). The idea is Dijkstraโs (โprivate
semaphoresโ); Hoare named it while inventing monitors. A CV
pthread_cond_t c; has two operations (weโll call them wait() and
signal()):
pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m); // sleep
pthread_cond_signal(pthread_cond_t *c); // wake one
Notice wait() takes a mutex. The rule that makes CVs work:
wait()assumes the lock is held when called;- it atomically releases the lock and puts the caller to sleep;
- when woken, it re-acquires the lock before returning.
That atomic โrelease-and-sleepโ is what prevents a whole class of races.
The correct join uses it, with a state variable done:
The signaler โ thr_exit()
void thr_exit() {
Pthread_mutex_lock(&m);
done = 1;
Pthread_cond_signal(&c);
Pthread_mutex_unlock(&m);
}The waiter โ thr_join()
void thr_join() {
Pthread_mutex_lock(&m);
while (done == 0)
Pthread_cond_wait(&c, &m);
Pthread_mutex_unlock(&m);
}Step through the handoff (the parent-first case; the reverse case is
easy โ if the child runs first, it sets done=1, signals nobody, and
the parentโs while (done==0) is simply false, so it never waits):
The lock is held around the whole check-and-wait โ that is what closes the race.
The same lifecycle as a state machine โ drive it and watch a thread move between running, sleeping on the CV, and merely ready:
Why both the state variable AND the lock are essential
Two tempting simplifications each break. Drop the state variable
(Figure 30.4): if the child runs first and signals when no one is
asleep, the signal is lost; the parent later calls wait() and sleeps
forever. The variable done is what lets the parent discover the child
already finished.
Drop the lock (Figure 30.5): now the check and the sleep arenโt atomic. Replay the deadly interleaving:
The parent (holding NO lock) checks done โ it is 0 โ and decides it must wait. But it has not called wait() yet.
Tip: Hold The Lock While Signaling (and Always While Waiting)
Holding the lock when you callsignal() is not strictly
required in every case, but itโs simplest and sometimes mandatory โ so
just do it. Holding the lock when you call wait() is
not optional: waitโs very semantics assume the lock is held,
release it on sleep, and re-acquire it on wake. Generalization: hold
the lock when calling signal or wait, and youโll always be in good
shape.With the mechanism understood, the next section stress-tests it on the classic producer/consumer problem โ where a single condition variable turns out not to be enough.
Check yourself: condition variables
1.pthread_cond_wait() takes a mutex. What does it do with it?
2.In Figure 30.4 the state variable done is removed. What goes wrong?
3.Figure 30.5 removes the lock around the check-and-wait. Trace the bug.
4.A thread calls signal() and wakes a sleeper. What state is the woken thread in immediately after?
5.Using the correct Figure 30.3 code, the child happens to run to completion BEFORE the parent reaches thr_join(). What happens?