Condition variables
Coordinate threads with POSIX condition variables — waiting for a condition, signalling, and the producer-consumer pattern.
- Initialise and use a pthread_cond_t condition variable
- Use pthread_cond_wait to block until a condition is true
- Use pthread_cond_signal and pthread_cond_broadcast to wake waiting threads
- Explain why pthread_cond_wait always requires a mutex
A mutex allows one thread to wait for exclusive access to shared data. But sometimes a thread needs to wait for a specific condition to become true — for example, "wait until there is work to do" or "wait until the buffer is not full." Polling with a locked mutex wastes CPU and prevents progress. Condition variables let threads sleep until another thread signals that the condition has changed.
The API
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; /* static init */
/* or */
pthread_cond_init(&cond, NULL);
pthread_cond_destroy(&cond);
/* Waiting: */
pthread_mutex_lock(&mtx);
while (!condition) {
pthread_cond_wait(&cond, &mtx); /* atomically releases mtx and sleeps */
}
/* condition is now true; mtx is re-locked */
pthread_mutex_unlock(&mtx);
/* Signalling: */
pthread_mutex_lock(&mtx);
/* modify shared state so condition becomes true */
pthread_cond_signal(&cond); /* wake one waiting thread */
/* or */
pthread_cond_broadcast(&cond); /* wake all waiting threads */
pthread_mutex_unlock(&mtx);Why pthread_cond_wait takes a mutex:
When pthread_cond_wait is called, it atomically releases the mutex and puts the thread to sleep. When a signal wakes the thread, it atomically re-acquires the mutex before returning. This ensures no signal is lost between checking the condition and going to sleep.
Why a while loop, not if:
Spurious wakeups are permitted by the standard — a thread may return from pthread_cond_wait without a signal. Always re-check the condition in a while loop.
Producer-consumer with a fixed-size buffer
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define BUFFER_SIZE 5
#define ITEMS_TOTAL 20
static int buffer[BUFFER_SIZE];
static int buf_count = 0;
static int buf_in = 0, buf_out = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
static pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
void produce(int item) {
pthread_mutex_lock(&mtx);
while (buf_count == BUFFER_SIZE) {
pthread_cond_wait(¬_full, &mtx); /* wait until not full */
}
buffer[buf_in] = item;
buf_in = (buf_in + 1) % BUFFER_SIZE;
buf_count++;
pthread_cond_signal(¬_empty); /* signal that buffer is not empty */
pthread_mutex_unlock(&mtx);
}
int consume(void) {
pthread_mutex_lock(&mtx);
while (buf_count == 0) {
pthread_cond_wait(¬_empty, &mtx); /* wait until not empty */
}
int item = buffer[buf_out];
buf_out = (buf_out + 1) % BUFFER_SIZE;
buf_count--;
pthread_cond_signal(¬_full); /* signal that buffer is not full */
pthread_mutex_unlock(&mtx);
return item;
}
void *producer(void *arg) {
(void)arg;
for (int i = 0; i < ITEMS_TOTAL; i++) {
produce(i);
printf("Produced: %d\n", i);
}
return NULL;
}
void *consumer(void *arg) {
(void)arg;
for (int i = 0; i < ITEMS_TOTAL; i++) {
int item = consume();
printf("Consumed: %d\n", item);
}
return NULL;
}
int main(void) {
pthread_t prod, cons;
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
return 0;
}The two condition variables coordinate the producer and consumer: not_full prevents the producer from overwriting data; not_empty prevents the consumer from reading from an empty buffer.
pthread_cond_broadcast vs. pthread_cond_signal
pthread_cond_signalwakes one waiting thread (implementation-defined which one).pthread_cond_broadcastwakes all waiting threads.
Use broadcast when the condition change affects multiple waiting threads — for example, when a read-write lock is released for reading (any number of readers can now proceed). Use signal when exactly one thread should proceed — for example, when an item is added to a queue for a single consumer.
Condition variables are the building block for semaphores, barriers, read-write locks, and event objects. All of these higher-level synchronisation primitives can be implemented using a mutex and a condition variable. The next module on the bounded queue challenge will put this into practice.
Where to go next
Next: deadlock and how to avoid it — what causes threads to block each other permanently and the design disciplines that prevent it.
Mutexes and race conditions
Protect shared data in C with POSIX mutexes — initialising, locking, and unlocking pthread_mutex_t to eliminate data races.
Deadlock and how to avoid it
Understand what causes deadlock in multithreaded C programs and apply the design disciplines — lock ordering, timeouts, and lock hierarchies — that prevent it.