Beginner’s Guide to Condition Variables in C

You’re asking for a clear, beginner-friendly English explanation of Condition Variable—a critical synchronization tool for multi-threading that enables threads to wait for a specific condition to be true (complementing mutexes/semaphores we covered earlier). I’ll break down its definition, core purpose, how it works, and provide a practical code example to illustrate its use.

1. Core Definition & Simple Explanation

  • Term: Condition Variable (Pronunciation: /kənˈdɪʃn ˈveəriəbl/)
  • Simple Definition:A condition variable is a synchronization primitive that allows threads to wait (block) until a specific condition is met (e.g., “a queue has data to process” or “a resource is available”). It works hand-in-hand with a mutex to ensure thread safety, and can wake up one or all waiting threads when the condition is satisfied.
  • Analogy for Beginners:Imagine a café (process) where baristas (threads) wait for customer orders (the “condition”):
    • Baristas lock a mutex (to avoid chaos) and check if there are orders. If no orders (condition false), they wait on a condition variable (go idle, release the mutex so others can act).
    • When a customer places an order (condition becomes true), the cashier signals the condition variable—waking a barista to process the order (the barista re-acquires the mutex and checks the condition again).

2. Core Concepts & Operations

Condition variables always require a mutex (to protect the shared state that defines the condition). Here are the key operations:

OperationExplanation
pthread_cond_wait()1. Releases the associated mutex (so other threads can modify the condition).2. Blocks the thread until another thread signals the condition variable.3. Re-acquires the mutex before returning (critical for thread safety).
pthread_cond_signal()Wakes up one waiting thread (if any) to check the condition.
pthread_cond_broadcast()Wakes up all waiting threads (useful if multiple threads need to react to the condition).

Critical Rule for Beginners

Always check the condition in a loop (not just once) after waking up—this avoids “spurious wakeups” (threads waking up even if the condition is still false, a rare but possible OS behavior).

3. Practical Code Example (Producer-Consumer Problem)

The producer-consumer problem is the classic use case for condition variables:

  • Producer Thread: Adds items to a shared queue (condition: “queue is not full”).
  • Consumer Thread: Removes items from the queue (condition: “queue is not empty”).
  • Condition variables ensure consumers wait if the queue is empty, and producers wait if the queue is full.

c

运行

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

// Shared queue configuration
#define QUEUE_SIZE 5
int queue[QUEUE_SIZE];
int queue_count = 0; // Tracks number of items in the queue (shared state)

// Synchronization primitives: mutex + condition variables
pthread_mutex_t queue_mutex;
pthread_cond_t cond_queue_not_empty; // For consumers (wait if queue is empty)
pthread_cond_t cond_queue_not_full;  // For producers (wait if queue is full)

// Producer thread: adds items to the queue
void* producer(void* arg) {
    long producer_id = (long)arg;
    int item = 0;

    while (item < 10) { // Produce 10 items total
        // Step 1: Lock mutex to protect shared queue state
        pthread_mutex_lock(&queue_mutex);

        // Step 2: Wait if queue is full (loop to handle spurious wakeups)
        while (queue_count == QUEUE_SIZE) {
            printf("Producer %ld: Queue full, waiting...\n", producer_id);
            pthread_cond_wait(&cond_queue_not_full, &queue_mutex);
        }

        // Step 3: Add item to queue (condition is true: queue not full)
        queue[queue_count++] = item;
        printf("Producer %ld: Added item %d (queue count: %d)\n", producer_id, item, queue_count);
        item++;

        // Step 4: Signal consumers (queue is now not empty)
        pthread_cond_signal(&cond_queue_not_empty);

        // Step 5: Unlock mutex
        pthread_mutex_unlock(&queue_mutex);

        // Simulate production time
        sleep(1);
    }

    pthread_exit(NULL);
}

// Consumer thread: removes items from the queue
void* consumer(void* arg) {
    long consumer_id = (long)arg;

    while (1) { // Run indefinitely (we'll stop it after all items are processed)
        // Step 1: Lock mutex to protect shared queue state
        pthread_mutex_lock(&queue_mutex);

        // Step 2: Wait if queue is empty (loop to handle spurious wakeups)
        while (queue_count == 0) {
            printf("Consumer %ld: Queue empty, waiting...\n", consumer_id);
            pthread_cond_wait(&cond_queue_not_empty, &queue_mutex);
        }

        // Step 3: Remove item from queue (condition is true: queue not empty)
        int item = queue[--queue_count];
        printf("Consumer %ld: Removed item %d (queue count: %d)\n", consumer_id, item, queue_count);

        // Step 4: Signal producers (queue is now not full)
        pthread_cond_signal(&cond_queue_not_full);

        // Step 5: Unlock mutex
        pthread_mutex_unlock(&queue_mutex);

        // Exit if all items (0-9) are processed
        if (item == 9) break;

        // Simulate consumption time
        sleep(2);
    }

    pthread_exit(NULL);
}

int main() {
    pthread_t prod_thread, cons_thread;

    // Initialize mutex and condition variables
    pthread_mutex_init(&queue_mutex, NULL);
    pthread_cond_init(&cond_queue_not_empty, NULL);
    pthread_cond_init(&cond_queue_not_full, NULL);

    // Create producer and consumer threads
    pthread_create(&prod_thread, NULL, producer, (void*)1);
    pthread_create(&cons_thread, NULL, consumer, (void*)1);

    // Wait for threads to finish
    pthread_join(prod_thread, NULL);
    pthread_join(cons_thread, NULL);

    // Cleanup
    pthread_mutex_destroy(&queue_mutex);
    pthread_cond_destroy(&cond_queue_not_empty);
    pthread_cond_destroy(&cond_queue_not_full);

    printf("\nAll items processed. Program completed.\n");
    return 0;
}

Compile & Run (Linux/macOS):

bash

运行

gcc condition_var_example.c -o condition_var_example -pthread
./condition_var_example

Expected Output (Coordinated Production/Consumption):

plaintext

Producer 1: Added item 0 (queue count: 1)
Consumer 1: Removed item 0 (queue count: 0)
Consumer 1: Queue empty, waiting...
Producer 1: Added item 1 (queue count: 1)
Producer 1: Added item 2 (queue count: 2)
Consumer 1: Removed item 1 (queue count: 1)
Producer 1: Added item 3 (queue count: 2)
Producer 1: Added item 4 (queue count: 3)
Consumer 1: Removed item 2 (queue count: 2)
Producer 1: Added item 5 (queue count: 3)
Producer 1: Added item 6 (queue count: 4)
Consumer 1: Removed item 3 (queue count: 3)
Producer 1: Added item 7 (queue count: 4)
Producer 1: Added item 8 (queue count: 5)
Producer 1: Queue full, waiting...
Consumer 1: Removed item 4 (queue count: 4)
Producer 1: Added item 9 (queue count: 5)
Producer 1: Queue full, waiting...
Consumer 1: Removed item 5 (queue count: 4)
Consumer 1: Removed item 6 (queue count: 3)
Consumer 1: Removed item 7 (queue count: 2)
Consumer 1: Removed item 8 (queue count: 1)
Consumer 1: Removed item 9 (queue count: 0)

All items processed. Program completed.

4. Key Use Cases for Condition Variables

  • Producer-Consumer Problems: Coordinate threads adding/removing items from a queue (the most common use case).
  • Wait-for-Event Scenarios: Threads wait for an event (e.g., “data received from network”, “file write completed”) instead of polling (checking repeatedly, which wastes CPU).
  • Thread Coordination: Synchronize multiple threads to run in a specific order (e.g., wait for all worker threads to finish before a main thread proceeds).

5. Condition Variable vs. Semaphore/Mutex

ToolPrimary PurposeKey Difference
MutexMutual exclusion (one thread at a time)No waiting for conditions—only locks resources.
SemaphoreLimit concurrent access to N resourcesCounts resources, not tied to a logical condition.
Condition VariableWait for a logical condition to be trueRequires a mutex; wakes threads only when the condition changes (avoids polling).

Summary

It’s the ideal tool for the producer-consumer problem and other scenarios where threads need to wait for events (not just resource availability).

Condition Variable lets threads wait for a specific logical condition (e.g., “queue not empty”) and is always used with a mutex for thread safety.

Core operations: pthread_cond_wait() (wait for condition), pthread_cond_signal() (wake one thread), pthread_cond_broadcast() (wake all threads).

Always check the condition in a loop after waking up to handle spurious wakeups—this is a critical best practice for beginners.



了解 Ruigu Electronic 的更多信息

订阅后即可通过电子邮件收到最新文章。

Posted in

Leave a comment