wisconsin
Lecture 13 - Condition Variables

Lecture 13 - Condition Variables

Curated by Last modified: 10 min read

Instructor: Mike Swift & Tej Chajed Topics: Condition variables, wait/signal, thread join, producer/consumer problem, concurrency bugs


What Are Condition Variables?

Condition Variable: A queue of waiting threads. Allows one thread to wait for a signal from another thread before proceeding.

  • Thread B waits for a signal on a CV before running: wait(CV, ...)
  • Thread A sends a signal to the CV when it is time for B to run: signal(CV, ...)

The wait() Operation

wait(cond_t *cv, mutex_t *lock)

  • Requires the lock is held when wait() is called
  • Puts the caller to sleep and releases the lock (atomically)
  • When awoken, reacquires the lock before returning

Lecture Note

The sequence inside wait() is: unlock(l) → sleeps → lock(l). The caller holds the lock before calling wait(), and holds it again after wait() returns.

The signal() Operation

signal(cond_t *cv)

  • Wakes a single waiting thread (if >= 1 thread is waiting)
  • If there is no waiting thread, just returns, doing nothing

Key Insight

Signals do not get remembered for future waits. If a condition is signaled and no thread is wait()ing, the signal is lost. It makes no difference.

Why Do Condition Variables Take a Lock?

  • Synchronization is around shared state
  • Locks are needed to prevent race conditions around shared state
  • The CV must release the lock atomically with sleeping, otherwise there is a window where a signal could be missed

Implementing Join with Condition Variables

The thread_join() / thread_exit() pattern demonstrates how to correctly use CVs. The lecture builds up through several broken attempts.

Attempt: No Locks

// Parent:
void thread_join() {
    if (done == 0)            // x
        Cond_wait(&c, &m);   // y
}

// Child:
void thread_exit() {
    done = 1;                 // a
    Cond_signal(&c);          // b
}

Problem: If the child runs first (a, b), the signal is sent before the parent calls wait(). Then the parent checks done == 0 (false after child set it to 1), but if the schedule is: child does a, then parent does x (seeing done == 0), then child does b (signal is lost), parent does y — parent waits forever.

Lecture Note

done is 0 before waiting, 1 after thread_exit. The annotation shows the problematic interleaving where Y waits forever.

Attempt 1: Lock Around Wait/Signal (No State Check)

// Parent:
void thread_join() {
    Mutex_lock(&m);           // x
    Cond_wait(&c, &m);       // y
    Mutex_unlock(&m);         // z
}

// Child:
void thread_exit() {
    Mutex_lock(&m);           // a
    Cond_signal(&c);          // b
    Mutex_unlock(&m);         // c
}

Works when parent runs first (x, y, then child a, b, c, then parent z). But breaks when child runs first: child does a, b (signal is ignored — nobody waiting), c; then parent does x, y — parent waits forever.

Attempt 2: State Check Without Lock in Child

// Parent:
void thread_join() {
    Mutex_lock(&m);           // w
    if (done == 0)            // x
        Cond_wait(&c, &m);   // y
    Mutex_unlock(&m);         // z
}

// Child:
void thread_exit() {
    done = 1;                 // a
    Cond_signal(&c);          // b
}

This fixes the case where the child runs first (parent sees done == 1, skips wait). But breaks with interleaving: parent does w, x (sees done == 0), then child does a, b (signal lost — parent hasn’t called wait() yet), then parent does y — parent sleeps forever.

Correct Implementation

// Parent:
void thread_join() {
    Mutex_lock(&m);           // w
    if (done == 0)            // x
        Cond_wait(&c, &m);   // y
    Mutex_unlock(&m);         // z
}

// Child:
void thread_exit() {
    Mutex_lock(&m);           // a
    done = 1;                 // b
    Cond_signal(&c);          // c
    Mutex_unlock(&m);         // d
}

The mutex ensures no race between checking/modifying state and calling wait/signal. Both threads hold the lock when interacting with the shared done variable and the condition variable.

Lecture Note

c is a cond_t (condition variable), m is a mutex_t. Use the mutex to ensure no race between interacting with state and wait/signal.


Rules of Thumb for Condition Variables

Rule 1: Keep State in Addition to CVs

  • CVs are used to signal threads when state changes
  • If state is already as needed, the thread does not wait for a signal
  • Without state, you cannot know whether to wait or proceed

Rule 2: Always Do Wait/Signal with Lock Held

  • The lock protects the shared state that the CV is associated with
  • Without the lock, there is a race between checking state and calling wait

Rule 3: Recheck Assumptions After Waking

  • Whenever a lock is acquired, recheck assumptions about state
  • Another thread could grab the lock in between the signal and the wakeup from wait
  • Some libraries also have “spurious wakeups” (may wake multiple waiting threads at signal or at any time)
  • This is why you use while() instead of if() to check the condition

Rule 4: One CV Per Condition

  • Have a separate condition variable for each condition you want to wait on
  • Signal the CV when that condition becomes true
  • Avoids waking up wrong threads unnecessarily
  • Ensures waking up the right threads

Lecture Note

Multiple condition variables can still share a single mutex.


Producer/Consumer Problem

Motivation: UNIX Pipes

A pipe may have many writers (producers) and readers (consumers). Internally, there is a finite-sized buffer (circular buffer with start and end pointers).

  • Writers add data to the buffer
    • Writers have to wait if buffer is full
  • Readers remove data from the buffer
    • Readers have to wait if buffer is empty

Lecture Note

The pipe buffer works like a circular queue. Writers advance end, readers advance start. When start == end, the buffer is empty. When the buffer wraps around and fills, writers must wait. Example: cat | grep — cat is the producer, grep is the consumer.

Implementation requirements:

  • Reads/writes to buffer require locking
  • When buffers are full, writers must wait
  • When buffers are empty, readers must wait

The General Problem

  • Producers generate data (like pipe writers)
  • Consumers grab data and process it (like pipe readers)
  • Producer/consumer problems are frequent in systems (e.g., web servers)
  • General strategy: use condition variables to make producers wait when buffers are full and consumers wait when there is nothing to consume

Single Producer, Single Consumer (One CV — Works)

Start with the easy case: 1 producer thread, 1 consumer thread, 1 shared buffer slot (max = 1). numfull tracks the number of filled buffers.

void *producer(void *arg) {
    for (int i = 0; i < loops; i++) {
        mutex_lock(&m);
        while (numfull == max)
            cond_wait(&cond, &m);
        do_fill(i);
        cond_signal(&cond);
        mutex_unlock(&m);
    }
}

void *consumer(void *arg) {
    while (1) {
        mutex_lock(&m);
        while (numfull == 0)
            cond_wait(&cond, &m);
        int tmp = do_get();
        cond_signal(&cond);
        mutex_unlock(&m);
        printf("%d\n", tmp);
    }
}

Lecture Note

numfull starts at 0, m is initially free, and cond queue is empty. The consumer sees the buffer is empty, sleeps. The producer acquires the lock, fills the buffer (numfull becomes 1), signals the consumer, unlocks. The consumer wakes, acquires the lock, gets the data, signals the producer, and the cycle repeats.

Two Consumers Problem (Why while Not if)

With 2 consumers and 1 producer using a single CV and if instead of while:

// BROKEN with 2 consumers — uses if instead of while
void *producer(void *arg) {
    for (int i = 0; i < loops; i++) {
        Mutex_lock(&m);       // p1
        if (numfull == max)   // p2  <-- BUG: should be while
            Cond_wait(&cond, &m); // p3
        do_fill(i);           // p4
        Cond_signal(&cond);   // p5
        Mutex_unlock(&m);     // p6
    }
}

void *consumer(void *arg) {
    while (1) {
        Mutex_lock(&m);       // c1
        if (numfull == 0)     // c2  <-- BUG: should be while
            Cond_wait(&cond, &m); // c3
        int tmp = do_get();   // c4
        Cond_signal(&cond);   // c5
        Mutex_unlock(&m);     // c6
        printf("%d\n", tmp);  // c7
    }
}

Problematic schedule:

  1. Consumer1 waits because numfull == 0
  2. Consumer2 waits because numfull == 0
  3. Producer fills buffer, signals — wakes Consumer1
  4. Before Consumer1 runs, Consumer2 grabs the lock, consumes the item, sets numfull = 0
  5. Consumer1 resumes after wait() but numfull is now 0 — reads bad data

Lecture Note

The condition may have changed before the woken thread actually runs. Use while() to retest the condition after waking up.

Another problem with a single CV: a consumer might signal another consumer instead of the producer, leading to all threads sleeping.

Correct Solution: Two CVs and While Loop

void *producer(void *arg) {
    for (int i = 0; i < loops; i++) {
        Mutex_lock(&m);               // p1
        while (numfull == max)         // p2
            Cond_wait(&empty, &m);     // p3
        do_fill(i);                    // p4
        Cond_signal(&fill);            // p5
        Mutex_unlock(&m);              // p6
    }
}

void *consumer(void *arg) {
    while (1) {
        Mutex_lock(&m);
        while (numfull == 0)
            Cond_wait(&fill, &m);
        int tmp = do_get();
        Cond_signal(&empty);
        Mutex_unlock(&m);
    }
}

Key properties:

  • No concurrent access to shared state (mutex protects it)
  • Every time the lock is acquired, assumptions are reevaluated (while loop)
  • A consumer will get to run after every do_fill() (signaled on fill)
  • A producer will get to run after every do_get() (signaled on empty)
  • Producers wait on empty, consumers wait on fill — signals always wake the right type of thread

Concurrency Bug Patterns

A study by Lu et al. [ASPLOS 2008] examined over 500K bug reports across four major projects (MySQL, Apache, Mozilla, OpenOffice) and identified common types of concurrency bugs:

  • Atomicity bugs — most common non-deadlock bug
  • Order bugs — less common but still significant
  • Deadlock bugs — very common, especially in Mozilla

Fixing Atomicity Bugs with Locks

// BUGGY: Thread 1 and Thread 2 have no synchronization
// Thread 1:
if (thd->proc_info) {
    ...
    fputs(thd->proc_info, ...);
    ...
}

// Thread 2:
thd->proc_info = NULL;

Thread 2 can set proc_info to NULL between Thread 1’s check and use, causing a crash. Fix: wrap both in a mutex for mutual exclusion.

// Thread 1:
pthread_mutex_lock(&lock);
if (thd->proc_info) {
    ...
    fputs(thd->proc_info, ...);
    ...
}
pthread_mutex_unlock(&lock);

// Thread 2:
pthread_mutex_lock(&lock);
thd->proc_info = NULL;
pthread_mutex_unlock(&lock);

Fixing Ordering Bugs with Condition Variables

// BUGGY: Thread 2 may run before Thread 1 finishes init
// Thread 1:
void init() {
    ...
    mThread = PR_CreateThread(mMain, ...);
    mtInit = 1;
    ...
}

// Thread 2:
void mMain(...) {
    ...
    mState = mThread->State;  // may crash if init not done
    ...
}

Fix: use a condition variable so Thread 2 waits until initialization is complete.

// Thread 1:
void init() {
    ...
    mThread = PR_CreateThread(mMain, ...);
    pthread_mutex_lock(&mtLock);
    mtInit = 1;
    pthread_cond_signal(&mtCond);
    pthread_mutex_unlock(&mtLock);
    ...
}

// Thread 2:
void mMain(...) {
    ...
    pthread_mutex_lock(&mtLock);
    while (mtInit == 0)
        Cond_wait(&mtCond, &mtLock);
    pthread_mutex_unlock(&mtLock);
    mState = mThread->State;
    ...
}

Summary

  • Condition variables are a synchronization primitive for threads to wait for something to be true
  • Rules of thumb for using CVs:
    1. Keep state in addition to CVs
    2. Always do wait/signal with lock held
    3. Whenever a thread wakes from waiting, recheck state (use while, not if)
    4. Have a condition variable per condition you want to wait on (can still share a mutex)
  • Key use case: producer-consumer queue
  • Use locks to fix atomicity bugs, condition variables to fix ordering bugs

Linked to this page