Using C Condition Variables for Producer Consumer
In multithreaded C programming, condition variables provide an
efficient mechanism for synchronizing threads that share resources,
avoiding the performance penalty of busy-waiting. When implementing a
producer-consumer pattern, producer threads generate data into a shared
buffer, while consumer threads retrieve it. By pairing POSIX condition
variables (pthread_cond_t) with mutex locks
(pthread_mutex_t), threads can safely communicate state
transitions—such as a buffer transitioning from empty to non-empty, or
full to available—ensuring race-free data access and optimal CPU
utilization.
The Synchronization Problem
In a classic bounded-buffer producer-consumer scenario, two boundary conditions threaten program stability:
- Underflow: Consumers attempting to read from an empty buffer.
- Overflow: Producers attempting to write to a full buffer.
Without synchronization, concurrent access results in data corruption or segmentation faults. While a mutex prevents simultaneous writes, using a mutex alone forces threads to repeatedly poll the buffer's status in a loop (busy-waiting), consuming unnecessary CPU cycles. Condition variables solve this by putting threads to sleep until the required state is achieved.
Core Components of POSIX Condition Variables
Coordinating threads safely requires three core elements:
- Mutex (
pthread_mutex_t): Enforces mutual exclusion so only one thread modifies the shared buffer, queue pointers, or element counters at any given time. - Condition Variables (
pthread_cond_t): Distinct signaling channels used to notify waiting threads:cond_nonempty: Signaled by producers to alert consumers that data is ready.cond_nonfull: Signaled by consumers to alert producers that buffer space is available.
- State Predicates: Internal boolean conditions
(e.g.,
count == 0orcount == BUFFER_SIZE) evaluated by threads to determine whether they must wait.
Essential POSIX Functions
pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex): Atomically releases the associated mutex and places the calling thread into a sleep state. When signaled, the thread wakes up and automatically reacquires the mutex before returning.pthread_cond_signal(pthread_cond_t *cond): Unblocks at least one thread currently waiting on the specified condition variable.pthread_cond_broadcast(pthread_cond_t *cond): Unblocks all threads currently waiting on the specified condition variable.
The Requirement for While Loops
Condition variable waits must always be wrapped in a
while loop rather than an if statement:
while (buffer_is_full) {
pthread_cond_wait(&cond_nonfull, &mutex);
}This construct protects against two distinct runtime issues:
- Spurious Wakeups: The operating system may occasionally wake a thread even if no signal was sent.
- Race on Wakeup: In multi-consumer or multi-producer environments, another thread may wake up first and alter the shared state before the current thread reacquires the mutex and runs. Re-evaluating the predicate ensures the condition remains valid.
Implementation Pattern
The following C implementation demonstrates a thread-safe bounded buffer using condition variables:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0;
int in = 0;
int out = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_nonempty = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_nonfull = PTHREAD_COND_INITIALIZER;
void produce(int item) {
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE) {
pthread_cond_wait(&cond_nonfull, &mutex);
}
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(&cond_nonempty);
pthread_mutex_unlock(&mutex);
}
int consume(void) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(&cond_nonempty, &mutex);
}
int item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
pthread_cond_signal(&cond_nonfull);
pthread_mutex_unlock(&mutex);
return item;
}Execution Flow
- Producer Workflow:
- Acquires the mutex to inspect
count. - If
count == BUFFER_SIZE, it callspthread_cond_wait(&cond_nonfull, &mutex). This atomically unlocks the mutex and suspends the producer. - Once awakened and the mutex is reacquired, it writes the item to
buffer[in], incrementsinandcount. - Calls
pthread_cond_signal(&cond_nonempty)to wake any sleeping consumer. - Releases the mutex.
- Acquires the mutex to inspect
- Consumer Workflow:
- Acquires the mutex to inspect
count. - If
count == 0, it callspthread_cond_wait(&cond_nonempty, &mutex), releasing the lock and sleeping. - Once awakened and re-verifying that
count > 0, it extractsbuffer[out], incrementsout, and decrementscount. - Calls
pthread_cond_signal(&cond_nonfull)to wake any sleeping producer. - Releases the mutex and processes the data.
- Acquires the mutex to inspect
This balance of atomic sleep-and-release mechanics, predicate verification, and explicit signaling allows condition variables to deliver safe, race-free, and high-performance producer-consumer execution in C.