In the previous section, we used Mutexes to prevent threads from stepping on each other’s toes while accessing shared data. But mutexes have a limitation: they are strictly for locking.
What if a thread needs to wait for something to happen? The Problem: “Busy Waiting”
Imagine the main thread of our Web Client wants to terminate only when all threads have finished downloading. We have a counter nlefttoread.
Without Condition Variables, the main thread would have to do this:
/* BAD CODE: Busy Waiting */
for ( ; ; ) {
Pthread_mutex_lock(&lock);
if (nlefttoread == 0) {
Pthread_mutex_unlock(&lock);
break; /* Done! */
}
Pthread_mutex_unlock(&lock);
/* Waste CPU cycles looping constantly */
}This is called busy waiting (or spinning). It burns CPU power just checking a value over and over again.
The Solution: Condition Variables
A Condition Variable allows a thread to go to sleep (block) until another thread signals that a specific condition has changed. It acts like a bell: one thread goes to sleep waiting for the bell, and another thread rings the bell when it’s time to wake up.
1. The Setup
Condition variables are always used in conjunction with a mutex.
- The Mutex: Protects the shared variable (e.g.,
nlefttoread). - The Condition Variable: Allows waiting for the state of that variable to change.
#include <pthread.h>
pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t ndone_cond = PTHREAD_COND_INITIALIZER;
int nlefttoread;2. The Main Thread (The Waiter)
The main thread wants to sleep until nlefttoread hits 0.
Pthread_mutex_lock(&ndone_mutex); // 1. Lock the mutex
while (nlefttoread > 0) { // 2. Check the condition
Pthread_cond_wait(&ndone_cond, &ndone_mutex); // 3. WAIT
}
Pthread_mutex_unlock(&ndone_mutex); // 4. Unlock and proceedThe Magic of pthread_cond_wait:
This function performs three steps atomically (all at once):
- Unlocks the mutex (so other threads can change the variable).
- Puts the thread to sleep (so it consumes no CPU).
- …Waits for a signal…
- When signaled, it wakes up, re-locks the mutex, and returns.
Note
You must always check the condition in a
whileloop, not anifstatement. This protects against “spurious wakeups” (where the system wakes the thread up for no reason) and race conditions where the condition might change back before the thread gets the lock.
3. The Worker Thread (The Signaler)
The worker thread does its job, updates the counter, and signals the main thread if appropriate.
void *do_get_read(void *vptr) {
/* ... Do the work ... */
Pthread_mutex_lock(&ndone_mutex); // 1. Lock
nlefttoread--; // 2. Update state
if (nlefttoread == 0) {
Pthread_cond_signal(&ndone_cond); // 3. Signal!
}
Pthread_mutex_unlock(&ndone_mutex); // 4. Unlock
}pthread_cond_signal: Wakes up one waiting thread.pthread_cond_broadcast: Wakes up all waiting threads (useful if multiple threads are waiting for the same event).