Thread / Synchronization · Linux userspace / kernel ABI

Mutexes and condition variables

Uses a bounded queue to explain a mutex-protected predicate, cond_wait's unlock-and-sleep operation, spurious wakeups, and lost wakeups.

Series
29 / 38
Build
cc -std=c17 -Wall -Wextra -O2 -pthread cond_queue.c -o cond_queue
Run
./cond_queue
Kernel
Linux 6.18.37 LTS

Does one pthread_cond_wait correspond exactly to one signal?

A condition variable does not store an event count. It tells waiters that a shared predicate may have become true. A waiter holds the mutex while checking the predicate in a while loop and lets pthread_cond_wait atomically release the mutex and prepare to sleep.

Sending a signal first does not leave a stored credit, and another thread may consume the predicate after wakeup. The waiter therefore checks again with while, not if.

Structure diagram

Figure 1. Layout of the predicate, mutex owner, and condition waiters
shared queue
value=73has_value=1stopped=0
mutex
owner: producerhandoffowner: consumer
condition waiters
consumer Aconsumer Btimeout waiter
after wake
mutex reacquirewhile predicateone consumer consumes value

A condition variable does not store events. The queue predicate lives under the mutex; after waking, a waiter reacquires the mutex and checks the predicate.

Call path

Figure 2. From userspace code to observable results
lock mutex protect predicate
while false recheck condition
cond_wait unlock + sleep
producer update change under mutex
signal/relock wake and recheck

The value of the predicate protected by the mutex is authoritative, not condition-variable state by itself. The queue structure determines signal timing and work-item lifetime.

Figure 3. Major points along the kernel-internal path
pthread mutex user atomic fast path
futex_wait contended sleep
producer store change predicate
futex_wake wake waiter
mutex acquire memory visibility

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
kernel/futex/waitwake.c futex_wait(), futex_wake() Sleep/wakeup foundation for pthread condition/mutex contention
kernel/futex/core.c get_futex_key() Convert a private/shared futex word into a wait-queue key
kernel/futex/pi.c futex_lock_pi() Kernel path for a priority-inheritance mutex

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 -pthread cond_queue.c -o cond_queue
01#include <pthread.h>
02#include <stdio.h>
03
04struct queue {
05    pthread_mutex_t lock;
06    pthread_cond_t ready;
07    int value;
08    int has_value;
09    int stopped;
10};
11
12static void *consumer(void *argument)
13{
14    struct queue *queue = argument;
15    pthread_mutex_lock(&queue->lock);
16    while (!queue->has_value && !queue->stopped)
17        pthread_cond_wait(&queue->ready, &queue->lock);
18    if (queue->has_value) {
19        printf("value=%d\n", queue->value);
20        queue->has_value = 0;
21    }
22    pthread_mutex_unlock(&queue->lock);
23    return NULL;
24}
25
26int main(void)
27{
28    struct queue queue = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, 0, 0, 0 };
29    pthread_t thread;
30    pthread_create(&thread, NULL, consumer, &queue);
31    pthread_mutex_lock(&queue.lock);
32    queue.value = 73;
33    queue.has_value = 1;
34    pthread_cond_signal(&queue.ready);
35    pthread_mutex_unlock(&queue.lock);
36    pthread_join(thread, NULL);
37    pthread_cond_destroy(&queue.ready);
38    pthread_mutex_destroy(&queue.lock);
39    return 0;
40}

Code notes

Source line 16while (!queue->has_value

Rechecks whether the predicate is true after a spurious wakeup or preemption by another consumer. The stop predicate is protected by the same lock.

Source line 17pthread_cond_wait

Releases the queue lock, registers and sleeps as a waiter, then reacquires the mutex before returning.

Source line 32queue.value = 73

Updates payload and has_value in the same mutex critical section so a consumer sees them consistently.

Source line 34pthread_cond_signal

Can wake at least one waiter, but stores no event when no waiter exists. Because the predicate is already true, a later waiter does not sleep.

Source line 37pthread_cond_destroy

Destroys the synchronization object only after join guarantees that all waiters are gone.

Detailed behavior

01

A lost wakeup occurs in the window between unlock and wait

If code checks the predicate, manually unlocks the mutex, and calls a separate sleep primitive, a producer can signal in between and the event is lost. cond_wait makes these two operations atomic at the protocol level.

The producer also changes the predicate under the same mutex, serializing it with the waiter's check.

02

Choosing signal or broadcast

When one waiter consumes one item, signal is appropriate. When every waiter's predicate can change, as with a global configuration change or shutdown, broadcast is required.

Reduce thundering-herd cost after broadcast through predicates and queue sharding.

03

Specify the timeout clock

Depending on the implementation, pthread_cond_timedwait's default absolute timeout has CLOCK_REALTIME behavior. Set CLOCK_MONOTONIC in the condition attribute to avoid wall-clock adjustments.

The mutex is held again after a timeout return, so perform one final predicate check before unlocking.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
predicateUpdated under the mutex whenever queue state changes and read by consumers under the same lockhas_value, stopped, count
pthread_mutex_tExists from init through destroy after every user has joinedowner, contention, robust/PI attr
pthread_cond_tUsed from waiter registration through signal/broadcast and destroyed after waiters disappearclock, waiter sequence

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
Occasionally waits foreverA signal outside the predicate protocol or an unlock-wait raceCheck mutex ownership for every state access
An empty queue is consumedif was used, or a spurious wakeup occurredwhile predicate loop
shutdown hangOnly some waiters are signaledstopped predicate + broadcast

Verify it yourself

  1. Create several consumers and compare the number awakened with the number that actually consumes work under signal and broadcast.
  2. Change while to if, repeatedly issue unnecessary broadcasts to stress progress without a predicate, then restore it.
  3. Add a CLOCK_MONOTONIC condition attribute and timedwait to implement a timeout independent of wall-clock changes.
Run./cond_queue
Tracestrace -f -e trace=futex,clone,exit ./cond_queue

Primary sources