Testing a Condition Variable’s Predicate

 

← Chapter 30 Index

30.2.3 — Testing a Condition Variable’s Predicate

Why you MUST use while, not if — spurious wakeups explained

Key Terms in This File

predicate while loop spurious wakeup loose predicate recheck multiple waiters

What Is a “Predicate”?

A predicate is the condition (involving one or more shared variables) that a thread is waiting for. For example, if a consumer waits for items to be available, the predicate is (avail > 0). The consumer waits while the predicate is false and proceeds when it becomes true.

The critical rule that every beginner must memorize:

Always use while, NEVER use if, when checking a condition variable predicate.

The Correct Pattern (while) vs The Wrong Pattern (if)

❌ WRONG — Using if

pthread_mutex_lock(&mtx);

if (avail == 0)             /* WRONG! */
    pthread_cond_wait(&cond, &mtx);

/* DANGER: avail might still be 0! */
avail--;  /* Could be wrong */

pthread_mutex_unlock(&mtx);

✅ CORRECT — Using while

pthread_mutex_lock(&mtx);

while (avail == 0)          /* CORRECT */
    pthread_cond_wait(&cond, &mtx);

/* Guaranteed: avail > 0 here */
avail--;

pthread_mutex_unlock(&mtx);

Why Must You Use while? Three Reasons

Reason 1: Another thread may grab the work first

Suppose two consumer threads (C1 and C2) are both waiting on avail == 0. The producer adds one item and calls broadcast() — both consumers wake up.

C1 gets the mutex first, finds avail == 1, consumes it, sets avail = 0, unlocks. Now C2 gets the mutex. With an if statement, C2 would proceed directly and try to consume from an empty queue — which is wrong. With a while loop, C2 re-checks: sees avail == 0, and correctly goes back to sleep.

Reason 2: “Loose” predicates

Sometimes it is simpler to signal a condition variable based on an approximation of the predicate’s state. For example, “there might be work” rather than “there definitely is work.” The waiting thread can verify for itself by re-checking the predicate after waking. A while loop handles this naturally.

Reason 3: Spurious wakeups

Spurious wakeup: A thread waiting on a condition variable may wake up even though no other thread called signal() or broadcast(). This is not a bug — it is explicitly permitted by the POSIX standard. It happens rarely and is a consequence of efficient implementation techniques on some multiprocessor hardware.

A while loop handles spurious wakeups correctly — the thread wakes up, re-checks the predicate, finds it false, and goes back to sleep. An if statement would cause the thread to proceed incorrectly with the predicate still false.

Code Example 1: Demonstrating the while-loop Pattern

#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;

/* Two consumers — shows why while is needed when broadcast is used */
void *consumer(void *arg)
{
    int id = *((int *) arg);
    int consumed = 0;

    while (consumed < 3) {
        pthread_mutex_lock(&mtx);

        /* MUST use while — could be woken by broadcast when avail is 0 */
        while (avail == 0) {
            printf("Consumer %d: waiting (avail=%d)\n", id, avail);
            pthread_cond_wait(&cond, &mtx);
            /* After waking, we re-check: maybe the other consumer got it */
        }

        /* We know avail > 0 HERE — guaranteed by while loop */
        avail--;
        consumed++;
        printf("Consumer %d: consumed item %d (avail=%d)\n",
               id, consumed, avail);

        pthread_mutex_unlock(&mtx);
    }

    printf("Consumer %d: finished\n", id);
    return NULL;
}

void *producer(void *arg)
{
    for (int i = 0; i < 6; i++) {
        struct timespec ts = {0, 500000000};  /* 500ms */
        nanosleep(&ts, NULL);

        pthread_mutex_lock(&mtx);
        avail++;
        printf("Producer: added item (avail=%d)\n", avail);
        pthread_mutex_unlock(&mtx);

        /* broadcast wakes BOTH consumers even though only one can consume */
        pthread_cond_broadcast(&cond);
    }
    return NULL;
}

int main(void)
{
    pthread_t prod, c1, c2;
    int id1 = 1, id2 = 2;

    pthread_create(&c1, NULL, consumer, &id1);
    pthread_create(&c2, NULL, consumer, &id2);
    pthread_create(&prod, NULL, producer, NULL);

    pthread_join(prod, NULL);
    pthread_join(c1, NULL);
    pthread_join(c2, NULL);
    return 0;
}

/*
 * When broadcast wakes both consumers but avail==1:
 * - Consumer 1 gets mutex, finds avail=1, consumes, sets avail=0
 * - Consumer 2 gets mutex, re-checks (while loop), finds avail=0,
 *   goes back to sleep — CORRECT!
 * - Without while loop, Consumer 2 would try avail-- on avail=0 — BUG!
 */

Code Example 2: Handling Spurious Wakeups

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

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

void *worker(void *arg)
{
    int id = *((int *) arg);

    pthread_mutex_lock(&mtx);

    /* This while loop handles ALL three wakeup scenarios:
     * 1. Normal wakeup — signal/broadcast called and job_count > 0
     * 2. Spurious wakeup — job_count is still 0, go back to sleep
     * 3. Multiple waiters — another worker got the job first
     */
    while (job_count == 0) {
        printf("Worker %d: sleeping (job_count=%d)\n", id, job_count);
        pthread_cond_wait(&cond, &mtx);
        printf("Worker %d: woke up, rechecking (job_count=%d)\n",
               id, job_count);
        /* Even if this is a spurious wakeup, we just loop and sleep again */
    }

    /* Guaranteed to be here only when job_count > 0 */
    job_count--;
    printf("Worker %d: processing job (job_count=%d)\n", id, job_count);

    pthread_mutex_unlock(&mtx);
    return NULL;
}

void *dispatcher(void *arg)
{
    struct timespec ts = {1, 0};
    nanosleep(&ts, NULL);

    pthread_mutex_lock(&mtx);
    job_count = 3;   /* Add 3 jobs */
    printf("Dispatcher: added %d jobs\n", job_count);
    pthread_mutex_unlock(&mtx);

    pthread_cond_broadcast(&cond);  /* Wake all workers */
    return NULL;
}

int main(void)
{
    pthread_t w[3], d;
    int ids[3] = {1, 2, 3};

    for (int i = 0; i < 3; i++)
        pthread_create(&w[i], NULL, worker, &ids[i]);
    pthread_create(&d, NULL, dispatcher, NULL);

    for (int i = 0; i < 3; i++)
        pthread_join(w[i], NULL);
    pthread_join(d, NULL);
    return 0;
}

/*
 * The while loop means: even if a spurious wakeup occurs,
 * the worker simply goes back to sleep. The code is correct
 * in all cases.
 */

Interview Questions — Predicate & Spurious Wakeups

Q1. What is a predicate in the context of condition variables? A predicate is the condition involving shared variables that a thread is waiting to become true. For example, for a consumer waiting for items, the predicate is (avail > 0). The thread should check this predicate before proceeding after waking from pthread_cond_wait().
Q2. Why must you always use a while loop, not an if statement, with pthread_cond_wait()? Three reasons: (1) Another thread may grab the resource between the signal and when you acquire the mutex. (2) You may want to use loose predicates where signaling means “maybe” not “definitely”. (3) Spurious wakeups can occur — pthread_cond_wait() may return even though no thread signaled it. A while loop handles all three by rechecking the predicate.
Q3. What is a spurious wakeup? A spurious wakeup is when pthread_cond_wait() returns even though no thread called pthread_cond_signal() or pthread_cond_broadcast(). It is a rare but explicitly allowed behavior in POSIX, caused by implementation choices on certain multiprocessor architectures. A while loop around pthread_cond_wait() handles it correctly.
Q4. Consider: two threads wait on a condition variable, one item is produced, and pthread_cond_broadcast() is called. What happens with an if statement vs a while loop? With broadcast, both threads wake. Thread A gets the mutex, consumes the item (avail=0), unlocks. Thread B gets the mutex. With if: Thread B skips the wait and tries to consume from empty queue — bug! With while: Thread B rechecks, finds avail=0, and goes back to sleep — correct!
Q5. What is a “loose predicate”? Signaling a condition variable to mean “there might be something for you” rather than “there definitely is something”. The woken thread verifies for itself by rechecking the predicate. This design approach simplifies signaling logic at the cost of occasional redundant wakeups, all safely handled by the while loop.
Q6. After pthread_cond_wait() returns, what is guaranteed? Only that pthread_cond_signal() or pthread_cond_broadcast() was called at some point (or a spurious wakeup occurred), and that the mutex is re-locked. The predicate’s state is NOT guaranteed to still be true. You must re-check it in the while loop.

Leave a Reply

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