Condition Variables: Introduction

 

← Chapter 30 Index

30.2 / 30.2.1 — Condition Variables: Introduction

The problem with busy-waiting — and how condition variables fix it

Key Terms in This File

condition variable pthread_cond_t PTHREAD_COND_INITIALIZER busy-waiting spin loop signal wait producer-consumer avail

What Problem Does a Condition Variable Solve?

Mutexes alone are not enough for all synchronization needs. Sometimes a thread does not just need exclusive access — it needs to wait for something to happen before it can proceed.

Example: A consumer thread can only work when a producer thread has made something available. Without condition variables, the consumer must sit in a loop constantly checking:

/* BAD: Busy-waiting wastes 100% of CPU */
while (avail == 0) {
    /* do nothing — just keep checking */
}

This is called busy-waiting or a spin loop. It wastes the entire CPU — the thread is “awake” doing nothing useful, preventing other threads from running.

A condition variable solves this: the waiting thread goes to sleep (releases the CPU) and is woken up only when another thread signals that the condition has changed.

❌ Without Condition Variable

Consumer loops endlessly checking avail. Thread is always “running” but doing nothing useful. Wastes CPU, slows other threads, generates heat.

✅ With Condition Variable

Consumer calls pthread_cond_wait() and goes to sleep. CPU is free for other work. Producer calls pthread_cond_signal() to wake the consumer exactly when needed.

The Busy-Wait Anti-Pattern (Code to Avoid)

Here is what a producer-consumer looks like without condition variables — the wrong way that wastes CPU:

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

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static int avail = 0;  /* Number of items ready for consumption */

/* Producer: creates items and signals availability */
void *producer(void *arg)
{
    for (int i = 0; i < 5; i++) {
        /* Produce an item (simulate with sleep) */
        struct timespec ts = {1, 0};
        nanosleep(&ts, NULL);

        pthread_mutex_lock(&mtx);
        avail++;
        printf("Producer: avail = %d\n", avail);
        pthread_mutex_unlock(&mtx);
    }
    return NULL;
}

/* Consumer: BAD VERSION — busy-waits, wastes CPU */
void *consumer_bad(void *arg)
{
    int total_consumed = 0;

    while (total_consumed < 5) {
        /* This loop runs millions of times wasting CPU */
        pthread_mutex_lock(&mtx);
        if (avail > 0) {
            avail--;
            total_consumed++;
            printf("Consumer: consumed item %d\n", total_consumed);
        }
        pthread_mutex_unlock(&mtx);
        /* No sleep — immediately tries again even if avail==0 */
    }
    return NULL;
}

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

/*
 * This works but the consumer burns 100% CPU spinning.
 * On a multicore system this wastes a whole core.
 * On a single core, it competes with the producer for CPU time.
 * The fix is condition variables (see ch30-08-condvar-signal-wait.html)
 */
Why busy-waiting is bad:
1. CPU waste — the spinning thread uses 100% of a CPU core doing nothing useful.
2. Power waste — important in embedded/mobile systems with battery constraints.
3. Starves other threads — on single-CPU systems, the spinning thread may prevent the producer from running.
4. Poor scalability — as the system gets busier, spin loops make things worse.

30.2.1 — Statically Allocated Condition Variables

Just like a mutex, a condition variable has its own type and must be initialized before use.

/* Condition variable type */ pthread_cond_t cond;

/* Static initialization (like PTHREAD_MUTEX_INITIALIZER for mutexes) */ pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

Same rule as mutexes: Never use a copy of a condition variable — always operate on the original. According to POSIX (SUSv3), using operations on a copy of a condition variable produces undefined behavior.

The Relationship Between Condition Variable and Mutex

A condition variable is always used together with a mutex. They work as a pair:

Mutex role Protects the shared variable from concurrent access (mutual exclusion)
Condition variable role Lets threads efficiently sleep until the shared variable reaches a desired state, and wake up when it does

The term “signal” used with condition variables is not related to Unix signals (SIGTERM, SIGKILL etc.). It simply means “notify” or “indicate” — to wake up a sleeping thread.

Code Example 2: Declaring a Mutex + Condition Variable Pair

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

/* Standard pattern: always declare cvar and mutex together */
static pthread_mutex_t mtx  = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;

/* The shared state that the condition variable protects */
static int data_ready = 0;    /* 0 = not ready, 1 = ready */
static int data_value = 0;

/* Producer: sets data_ready and signals the condition */
void *producer(void *arg)
{
    struct timespec ts = {2, 0};
    nanosleep(&ts, NULL);  /* Simulate producing data */

    pthread_mutex_lock(&mtx);
    data_value = 42;
    data_ready = 1;           /* Set the shared state */
    pthread_mutex_unlock(&mtx);

    pthread_cond_signal(&cond);  /* Wake the waiting consumer */
    printf("Producer: signaled, data = %d\n", data_value);
    return NULL;
}

/* Consumer: waits for data_ready to become 1 */
void *consumer(void *arg)
{
    pthread_mutex_lock(&mtx);

    /* Sleep until data_ready == 1 */
    while (data_ready == 0)
        pthread_cond_wait(&cond, &mtx);  /* Atomically unlocks mtx and sleeps */

    /* Now data is ready */
    printf("Consumer: woke up, data = %d\n", data_value);

    pthread_mutex_unlock(&mtx);
    return NULL;
}

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

/*
 * Compile: gcc -o condvar_intro condvar_intro.c -lpthread
 * Output:
 *   Producer: signaled, data = 42
 *   Consumer: woke up, data = 42
 *
 * Consumer does NOT spin — it sleeps until woken.
 */

Interview Questions — Condition Variables Introduction

Q1. What is busy-waiting and why is it bad? Busy-waiting (spinning) is when a thread repeatedly checks a condition in a tight loop without sleeping. It is bad because it wastes 100% of a CPU core doing nothing productive, starves other threads of CPU time, increases power consumption, and scales poorly under load.
Q2. What problem does a condition variable solve that a mutex alone cannot? A mutex ensures exclusive access to shared data, but cannot make a thread efficiently wait for a change in state. A condition variable allows a thread to sleep (releasing the CPU) until another thread notifies it that the state has changed — without busy-waiting.
Q3. What data type is used for a condition variable in pthreads? pthread_cond_t. It is initialized statically with PTHREAD_COND_INITIALIZER or dynamically with pthread_cond_init().
Q4. Why is a condition variable always used together with a mutex? The condition variable is used to signal changes in a shared variable’s state. That shared variable must also be protected from concurrent access by a mutex. The mutex and condition variable together provide both protection (mutex) and efficient notification (cond var). They are inseparable — pthread_cond_wait() takes both as arguments.
Q5. Does “signal” in the context of condition variables mean a Unix signal? No. The word “signal” here simply means “notify” or “indicate” — wake up a waiting thread. It has nothing to do with Unix/POSIX signals like SIGTERM or SIGINT.
Q6. Can you use a copy of a condition variable? No. Like mutexes, condition variables must always be used via the original initialized object. Using a copy produces undefined behavior per POSIX.

Leave a Reply

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