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:

  1. Underflow: Consumers attempting to read from an empty buffer.
  2. 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:

  1. Mutex (pthread_mutex_t): Enforces mutual exclusion so only one thread modifies the shared buffer, queue pointers, or element counters at any given time.
  2. 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.
  3. State Predicates: Internal boolean conditions (e.g., count == 0 or count == 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

  1. Producer Workflow:
    • Acquires the mutex to inspect count.
    • If count == BUFFER_SIZE, it calls pthread_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], increments in and count.
    • Calls pthread_cond_signal(&cond_nonempty) to wake any sleeping consumer.
    • Releases the mutex.
  2. Consumer Workflow:
    • Acquires the mutex to inspect count.
    • If count == 0, it calls pthread_cond_wait(&cond_nonempty, &mutex), releasing the lock and sleeping.
    • Once awakened and re-verifying that count > 0, it extracts buffer[out], increments out, and decrements count.
    • Calls pthread_cond_signal(&cond_nonfull) to wake any sleeping producer.
    • Releases the mutex and processes the data.

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.