Signaling and Waiting on Condition Variables

 

← Chapter 30 Index

30.2.2 — Signaling and Waiting on Condition Variables

pthread_cond_signal, broadcast, wait, timedwait — the complete producer-consumer pattern

Key Terms in This File

pthread_cond_signal pthread_cond_broadcast pthread_cond_wait pthread_cond_timedwait atomic unlock+sleep signal vs broadcast wait morphing ETIMEDOUT

The Four Condition Variable Functions

#include <pthread.h>

/* Wake ONE waiting thread */ int pthread_cond_signal(pthread_cond_t *cond);

/* Wake ALL waiting threads */ int pthread_cond_broadcast(pthread_cond_t *cond);

/* Sleep until cond is signaled (atomically unlock mutex while sleeping) */ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

/* Same as wait but with a timeout */ int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

/* All return 0 on success, or a positive error number on error */

pthread_cond_wait() — The Heart of the Pattern

pthread_cond_wait() does three things atomically (as one indivisible unit):

1
Unlock the mutex — so other threads (like the producer) can access the shared variable and change its state.
2
Block (sleep) — the thread goes to sleep, releasing the CPU. It stays asleep until another thread calls pthread_cond_signal() or pthread_cond_broadcast() on this condition variable.
3
Re-lock the mutex — when the thread wakes up, it automatically re-acquires the mutex before returning.
Why must steps 1 and 2 be atomic?
If “unlock mutex” and “go to sleep” were two separate steps, there would be a window where: the mutex is unlocked, but the thread has not yet started sleeping. The producer could run, change the state, and call signal() — but the consumer is not yet sleeping so the signal is lost. The consumer then sleeps forever waiting for a signal that already happened. Making steps 1 and 2 atomic eliminates this race condition.

signal() vs broadcast() — Which One to Use?

pthread_cond_signal()

Wakes at least one waiting thread. Use when all waiting threads do the same job and only one needs to run (e.g., one consumer to consume one item). More efficient than broadcast when this assumption holds.

pthread_cond_broadcast()

Wakes all waiting threads. Use when different threads might be waiting for different conditions on the same variable, or when you want to be safe and wake everyone. All awakened threads that have no work to do will simply go back to sleep.

broadcast() is always safe. Since waiting threads must re-check their condition in a while loop (covered in section 30.2.3), threads woken unnecessarily by broadcast will simply re-test the condition, find it false, and go back to sleep. The cost is some extra context switching.

The Standard Wait Pattern

The correct pattern for a thread waiting on a condition variable always looks like this:

pthread_mutex_lock(&mtx);

while (/* shared variable NOT in desired state */)
    pthread_cond_wait(&cond, &mtx);

/* Now shared variable IS in desired state — do work */

pthread_mutex_unlock(&mtx);

The reason for while instead of if is covered in detail in section 30.2.3 (spurious wakeups). For now, just remember: always use while.

Code Example 1: Complete Producer-Consumer with Condition Variable

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

static pthread_mutex_t mtx  = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;
static int avail = 0;   /* Number of items ready to consume */

#define NUM_ITEMS 10

/* Producer: produces items one at a time */
void *producer(void *arg)
{
    for (int i = 1; i <= NUM_ITEMS; i++) {
        /* Simulate work to produce one item */
        struct timespec ts = {0, 200000000};  /* 200ms */
        nanosleep(&ts, NULL);

        /* Add the item */
        pthread_mutex_lock(&mtx);
        avail++;
        printf("Producer: produced item %d (avail=%d)\n", i, avail);
        pthread_mutex_unlock(&mtx);

        /* Signal the consumer that something is available */
        pthread_cond_signal(&cond);
    }
    return NULL;
}

/* Consumer: waits for items and consumes them */
void *consumer(void *arg)
{
    int total = 0;

    while (total < NUM_ITEMS) {
        pthread_mutex_lock(&mtx);

        /* Sleep while nothing is available */
        while (avail == 0)
            pthread_cond_wait(&cond, &mtx);

        /* Consume all available items */
        while (avail > 0) {
            avail--;
            total++;
            printf("Consumer: consumed item %d (avail=%d)\n", total, avail);
        }

        pthread_mutex_unlock(&mtx);
    }

    printf("Consumer: done, total consumed = %d\n", total);
    return NULL;
}

int main(void)
{
    pthread_t prod, cons;
    pthread_create(&cons, NULL, consumer, NULL);
    pthread_create(&prod, NULL, producer, NULL);
    pthread_join(prod, NULL);
    pthread_join(cons, NULL);
    return 0;
}

/*
 * Compile: gcc -o producer_consumer producer_consumer.c -lpthread
 *
 * Consumer SLEEPS while avail==0 (no CPU waste).
 * Producer wakes it up exactly when an item is ready.
 */

Signal Timing: Before or After Unlock?

In the producer code above, we unlock the mutex first, then signal. POSIX allows either order:

  • Unlock then signal (recommended) — the waiting thread won’t immediately block on the mutex when it wakes up. Slightly better performance.
  • Signal then unlock — also correct. Some implementations use “wait morphing” to handle this efficiently.
Wait morphing: Some pthreads implementations use a technique called “wait morphing” — when signal is called while the mutex is still locked, the signaled thread is moved from the condition variable’s wait queue directly to the mutex’s wait queue without a context switch. This avoids an extra context switch in the “signal then unlock” case.

pthread_cond_timedwait() — Waiting With a Deadline

#include <stdio.h>
#include <time.h>
#include <errno.h>
#include <pthread.h>

static pthread_mutex_t mtx  = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;
static int data_ready = 0;

void *consumer_with_timeout(void *arg)
{
    struct timespec deadline;

    pthread_mutex_lock(&mtx);

    while (data_ready == 0) {
        /* Compute deadline: 3 seconds from now */
        clock_gettime(CLOCK_REALTIME, &deadline);
        deadline.tv_sec += 3;

        int s = pthread_cond_timedwait(&cond, &mtx, &deadline);

        if (s == ETIMEDOUT) {
            printf("Consumer: timed out waiting for data!\n");
            pthread_mutex_unlock(&mtx);
            return NULL;
        }
    }

    printf("Consumer: got data!\n");
    pthread_mutex_unlock(&mtx);
    return NULL;
}

void *slow_producer(void *arg)
{
    /* This producer takes 5 seconds — will cause consumer timeout */
    struct timespec ts = {5, 0};
    nanosleep(&ts, NULL);

    pthread_mutex_lock(&mtx);
    data_ready = 1;
    pthread_mutex_unlock(&mtx);
    pthread_cond_signal(&cond);
    printf("Producer: (too late) signaled\n");
    return NULL;
}

int main(void)
{
    pthread_t prod, cons;
    pthread_create(&cons, NULL, consumer_with_timeout, NULL);
    pthread_create(&prod, NULL, slow_producer, NULL);
    pthread_join(cons, NULL);
    pthread_join(prod, NULL);
    return 0;
}

/*
 * Output:
 *   Consumer: timed out waiting for data!
 *   Producer: (too late) signaled
 */

Interview Questions — Signal, Broadcast and Wait

Q1. What does pthread_cond_wait() do? List all three steps. (1) Atomically unlocks the associated mutex. (2) Blocks the thread (puts it to sleep). (3) When woken by a signal, automatically re-locks the mutex before returning. Steps 1 and 2 are atomic — no signal can be missed between them.
Q2. What is the difference between pthread_cond_signal() and pthread_cond_broadcast()? signal() wakes at least one waiting thread. broadcast() wakes all waiting threads. Use signal() when all waiters do the same job and one is enough. Use broadcast() when waiters may have different tasks or when safety is more important than efficiency.
Q3. Why must pthread_cond_wait() unlock the mutex and sleep atomically? To prevent a lost-signal race condition. If unlock and sleep were separate steps, the producer could run between them: unlock mutex → producer runs → producer signals → consumer starts sleeping. The signal is lost and the consumer waits forever. Atomic unlock+sleep ensures this cannot happen.
Q4. What is the state of the mutex when pthread_cond_wait() returns? The mutex is locked (held by the calling thread). pthread_cond_wait() always re-locks the mutex before returning, so the caller can safely inspect the shared variable.
Q5. What error does pthread_cond_timedwait() return on timeout? ETIMEDOUT. When it returns ETIMEDOUT, the mutex is still re-locked (the function always re-locks before returning, even on timeout).
Q6. Can you signal a condition variable before any thread is waiting on it? Yes, but the signal is lost — a condition variable holds no state. If no thread is currently waiting, the signal disappears. Any thread that subsequently calls pthread_cond_wait() will block until the condition is signaled again.
Q7. Is it better to signal before or after unlocking the mutex? Both orderings are correct per POSIX. Unlocking before signaling is generally recommended as it avoids a potential extra context switch (the woken thread won’t immediately block on the locked mutex). Some implementations use “wait morphing” to handle the reverse order efficiently.
Q8. Can multiple threads call pthread_cond_wait() on the same condition variable with different mutexes? No. All threads that wait concurrently on the same condition variable must use the same mutex. Using different mutexes for the same condition variable in concurrent waits has undefined behavior per POSIX.

Leave a Reply

Your email address will not be published. Required fields are marked *