Code of the Day
AdvancedConcurrency with pthreads

Condition variables

Coordinate threads with POSIX condition variables — waiting for a condition, signalling, and the producer-consumer pattern.

CAdvanced11 min read
By the end of this lesson you will be able to:
  • 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(&not_full, &mtx); /* wait until not full */
    }
    buffer[buf_in] = item;
    buf_in = (buf_in + 1) % BUFFER_SIZE;
    buf_count++;
    pthread_cond_signal(&not_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(&not_empty, &mtx); /* wait until not empty */
    }
    int item = buffer[buf_out];
    buf_out = (buf_out + 1) % BUFFER_SIZE;
    buf_count--;
    pthread_cond_signal(&not_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_signal wakes one waiting thread (implementation-defined which one).
  • pthread_cond_broadcast wakes 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.

Finished reading? Mark it complete to track your progress.

On this page