Chapter 69: Pthreads: Condition Variables for Signaling Between Threads

Chapter Objectives

Upon completing this chapter, you will be able to:

  • Understand the limitations of mutexes for complex thread synchronization scenarios.
  • Explain the role of condition variables in signaling between threads to coordinate work.
  • Implement thread-safe producer-consumer patterns using pthread_cond_waitpthread_cond_signal, and pthread_cond_broadcast.
  • Correctly initialize and destroy condition variables and their associated mutexes using pthread_cond_init and pthread_cond_destroy.
  • Analyze and debug common issues in concurrent programs, including spurious wakeups and lost signals.
  • Design and build multi-threaded applications on a Raspberry Pi 5 that require efficient inter-thread communication.

Introduction

In our exploration of concurrent programming, we have seen how mutexes are essential for protecting shared resources from simultaneous access, thereby preventing data corruption and race conditions. However, mutexes are fundamentally a locking mechanism; they provide mutual exclusion, but they do not offer a way for threads to notify each other about changes in the state of the system. Consider a scenario where one thread, a “producer,” generates data and adds it to a queue, while another thread, a “consumer,” retrieves data from that queue. If the queue is empty, the consumer must wait. How does it wait efficiently? It could repeatedly lock a mutex, check the queue, and unlock it if it’s empty—a technique known as busy-waiting or spinning. This approach is incredibly wasteful, consuming CPU cycles that could be used for productive work.

This is the problem that condition variables solve. They are synchronization primitives that allow threads to block, or wait, until some specific condition becomes true. They provide a mechanism for one thread to signal another that the state of the shared data has changed, allowing the waiting thread to wake up and proceed. Condition variables work hand-in-hand with mutexes to build sophisticated and efficient synchronization patterns. The mutex protects the shared data during access, while the condition variable manages the signaling and waiting based on the state of that data. This chapter will delve into the POSIX threads (Pthreads) API for condition variables, exploring the fundamental functions—pthread_cond_initpthread_cond_waitpthread_cond_signal, and pthread_cond_broadcast—that enable this powerful form of inter-thread communication. By the end of this chapter, you will be able to implement robust, efficient, and complex multi-threaded applications, such as the classic producer-consumer model, on your Raspberry Pi 5.

Technical Background

To fully appreciate the utility of condition variables, we must first solidify our understanding of their relationship with mutexes. A condition variable is not a data protection mechanism. It does not, by itself, prevent race conditions when accessing shared data. That role belongs exclusively to the mutex. Instead, a condition variable is a signaling mechanism. It allows threads to atomically release a mutex and go to sleep, waiting for a signal that the condition they are interested in might now be true.

This cooperative relationship is the cornerstone of their use: a thread must always hold a lock on a mutex before it can wait on a condition variable associated with that mutex. This design prevents a critical race condition. Imagine if a thread could check a condition (e.g., is_buffer_empty), decide to wait, but before it could actually go to sleep, another thread modified the condition and sent a signal. The signal would be lost, and the first thread might sleep forever. By requiring the mutex to be held, the entire operation of checking the condition and entering the wait state becomes atomic, preventing any other thread from intervening.

The Lifecycle of a Condition Variable

Like other Pthreads objects, a condition variable must be initialized before use and should be destroyed when it is no longer needed. The primary function for this is pthread_cond_init(). It initializes the condition variable object, pointed to by its first argument, cond. The second argument, attr, allows for specifying attributes for the condition variable. For most applications in embedded Linux, passing NULL for the attributes is sufficient, which applies the default settings.

C
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
                      const pthread_condattr_t *restrict attr);

Alternatively, for statically allocated condition variables, Pthreads provides a convenient macro, PTHREAD_COND_INITIALIZER, which initializes the variable with default attributes. This is often simpler for global or static variables.

C
pthread_cond_t my_cond = PTHREAD_COND_INITIALIZER;

Once all threads have finished using the condition variable, it is crucial to release the resources it holds by calling pthread_cond_destroy(). Attempting to destroy a condition variable while one or more threads are waiting on it results in undefined behavior, which is why proper program shutdown logic is critical.

Waiting for a Condition: pthread_cond_wait()

The most complex and vital function in the condition variable API is pthread_cond_wait(). This is where the magic of efficient waiting happens.

C
int pthread_cond_wait(pthread_cond_t *restrict cond,
                      pthread_mutex_t *restrict mutex);

When a thread calls pthread_cond_wait(), it must already have the mutex locked. The function then performs three critical actions atomically:

  1. It unlocks the mutex.
  2. It puts the calling thread to sleep, adding it to a list of threads waiting on the cond.
  3. When the thread is later woken up by a signal, it re-acquires the mutex before returning.

The atomicity of unlocking the mutex and entering the wait state is paramount. It ensures that no other thread can acquire the mutex and change the program’s state between the moment the waiting thread decides the condition is false and the moment it actually goes to sleep.

sequenceDiagram
    actor ThreadA
    participant Mutex
    participant ConditionVariable
    actor ThreadB
    
    Note over ThreadA, ThreadB: pthread_cond_wait Atomic Operation

    ThreadA->>+Mutex: Lock Mutex
    Note right of ThreadA: Checks shared condition<br/>(e.g., buffer is empty)
    
    rect rgb(255, 235, 235)
        Note over ThreadA, ConditionVariable: pthread_cond_wait(&cond, &mutex) called
        ThreadA->>Mutex: 1. Atomically Unlock Mutex
        deactivate Mutex
        ThreadA->>ConditionVariable: 2. Enter Waiting State
    end

    Note over ThreadB: Other threads can now run
    ThreadB->>+Mutex: Lock Mutex
    ThreadB->>+ThreadB: Change shared condition<br/>(e.g., add item to buffer)
    ThreadB->>ConditionVariable: pthread_cond_signal(&cond)
    ThreadB->>-Mutex: Unlock Mutex

    rect rgb(235, 255, 235)
        Note over ThreadA, ConditionVariable: Woken up by signal
        ConditionVariable-->>ThreadA: Wake up
        ThreadA->>Mutex: 3. Re-acquire Mutex before returning
        activate Mutex
    end
    
    Note right of ThreadA: Re-checks condition in while loop<br/>and proceeds safely
    ThreadA->>Mutex: Unlock Mutex
    deactivate Mutex

A subtle but critically important detail of pthread_cond_wait() is the concept of spurious wakeups. A thread waiting on a condition variable may occasionally wake up even if no signal was sent by another thread. This can happen for various reasons related to the underlying kernel implementation. Because of this possibility, it is an absolute rule that any call to pthread_cond_wait() must be enclosed in a while loop.

Consider this incorrect usage:

C
// Incorrect: Using 'if'
pthread_mutex_lock(&mutex);
if (resource_is_not_ready) {
    pthread_cond_wait(&cond, &mutex);
}
// ... proceed to use resource ...
pthread_mutex_unlock(&mutex);

If the thread experiences a spurious wakeup, it will exit pthread_cond_wait(), re-acquire the mutex, and incorrectly assume the resource is ready. The if statement is only checked once.

The correct pattern is:

C
// Correct: Using 'while'
pthread_mutex_lock(&mutex);
while (resource_is_not_ready) {
    pthread_cond_wait(&cond, &mutex);
}
// ... proceed to use resource ...
pthread_mutex_unlock(&mutex);

With the while loop, if the thread wakes up spuriously, it will simply re-evaluate the condition (resource_is_not_ready). If the condition is still not met, it will call pthread_cond_wait() again, correctly putting itself back to sleep. This loop structure makes the code robust against spurious wakeups and ensures the logic is sound.

Signaling Threads: pthread_cond_signal() and pthread_cond_broadcast()

Once a thread has changed the state of the shared data, it needs a way to notify any threads that might be waiting for this change. Pthreads provides two functions for this purpose: pthread_cond_signal() and pthread_cond_broadcast().

C
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

The pthread_cond_signal() function wakes up at least one thread that is currently waiting on the specified condition variable. If no threads are waiting, the signal has no effect and is simply lost. The choice of which waiting thread to wake up is left to the scheduling policy, so one should not make any assumptions about fairness or order (e.g., FIFO). This function is most appropriate when any of the waiting threads can handle the task. For instance, in a producer-consumer scenario with one item added to a queue, only one consumer needs to be woken up to process it. Using pthread_cond_signal() is more efficient in this case as it avoids the “thundering herd” problem, where many threads wake up, contend for the mutex, and all but one find they have to go back to sleep.

In contrast, pthread_cond_broadcast() wakes up all threads currently waiting on the condition variable. These woken threads will then contend for the associated mutex. Once a thread acquires the mutex, it will re-check the condition inside its while loop. This function is necessary when the signal might enable multiple threads to proceed. A classic example is a barrier, where all threads in a group must wait until the last thread arrives. Once the last thread arrives, it calls pthread_cond_broadcast() to release all the other waiting threads simultaneously. Another use case is when different waiting threads are waiting on different sub-conditions. A single state change might satisfy multiple threads, so broadcasting is the only way to ensure they all get a chance to re-evaluate their specific conditions.

A crucial best practice is to call these signaling functions after modifying the shared data but before unlocking the mutex. While it is technically possible to unlock the mutex first and then signal, it can lead to a performance pessimization. If you signal after unlocking, a woken thread might immediately run, try to acquire the mutex, but find that the signaling thread still holds it, forcing an immediate context switch. By signaling while still holding the mutex, you ensure that the woken thread will only be scheduled after the current thread releases the lock, leading to smoother execution.

graph TD


    subgraph "Scenario: pthread_cond_signal()"
        direction LR

        A1(Thread A<br><i>Waiting</i>)
        A2(Thread B<br><i>Waiting</i>)
        A3(Thread C<br><i>Waiting</i>)
        
        S1[Signal Sent] -->|Wakes ONE| A1
        S1 --> A2
        S1 --> A3

        A1 --> R1{Thread A<br><b>Ready</b>}
        
        style A1 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
        style A2 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
        style A3 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
        style S1 fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff
        style R1 fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
    end

    subgraph "Scenario: pthread_cond_broadcast()"
        direction LR
        B1(Thread A<br><i>Waiting</i>)
        B2(Thread B<br><i>Waiting</i>)
        B3(Thread C<br><i>Waiting</i>)

        S2[Broadcast Sent] -->|Wakes ALL| B1
        S2 --> B2
        S2 --> B3

        B1 --> R2{Thread A<br><b>Ready</b>}
        B2 --> R3{Thread B<br><b>Ready</b>}
        B3 --> R4{Thread C<br><b>Ready</b>}

        style B1 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
        style B2 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
        style B3 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
        style S2 fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff
        style R2 fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
        style R3 fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
        style R4 fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
    end

    

Function Purpose Key Parameters & Notes
pthread_cond_init() Initializes a condition variable object. cond: Pointer to the pthread_cond_t object.
attr: Attributes (usually NULL for defaults).
– Can also use PTHREAD_COND_INITIALIZER for static variables.
pthread_cond_destroy() Destroys a condition variable, freeing its resources. – Must not be called if any threads are currently waiting on the variable.
pthread_cond_wait() Atomically unlocks a mutex and waits for a signal. CRITICAL: Must be called inside a while loop to handle spurious wakeups.
– The mutex must be locked by the calling thread before entry.
pthread_cond_signal() Wakes up at least one thread waiting on the condition. – If no threads are waiting, the signal is lost.
– Use when any single waiting thread can handle the task. More efficient.
pthread_cond_broadcast() Wakes up all threads waiting on the condition. – Use when a state change may allow multiple threads to proceed (e.g., thread barrier).
– Can cause a “thundering herd” if used unnecessarily.
pthread_cond_timedwait() Like wait, but returns if not signaled by a specific absolute time. – Useful for preventing indefinite blocking.
– Timeout is specified with a struct timespec.

Practical Examples

Theory becomes concrete with practice. We will now implement the canonical producer-consumer problem on the Raspberry Pi 5. In this example, we will have one producer thread that creates “items” (represented by integers) and places them into a fixed-size shared buffer. We will have one consumer thread that removes these items and processes them. We will use a mutex to protect the buffer and two condition variables: one for the producer to wait on when the buffer is full (cond_full), and one for the consumer to wait on when the buffer is empty (cond_empty).

File Structure

For this project, our file structure will be simple. All the code will reside in a single source file, and we’ll use a Makefile for easy compilation.

Plaintext
producer-consumer/
├── Makefile
└── producer_consumer.c

Build and Configuration Steps

First, ensure you have the necessary build tools on your Raspberry Pi 5. If not, install them via apt:

Bash
sudo apt update
sudo apt install build-essential

Next, create the producer_consumer.c file and the Makefile.

Makefile

Makefile
# Makefile for the Producer-Consumer example

# Compiler
CC = gcc

# Compiler flags
# -g: Add debug information
# -Wall: Turn on all warnings
# -pthread: Link with the Pthreads library
CFLAGS = -g -Wall -pthread

# The target executable
TARGET = producer_consumer

# The source file
SOURCES = producer_consumer.c

# Default rule
all: $(TARGET)

# Rule to build the target
$(TARGET): $(SOURCES)
	$(CC) $(CFLAGS) -o $(TARGET) $(SOURCES)

# Rule to clean up the directory
clean:
	rm -f $(TARGET)

This Makefile defines the compiler (gcc), the necessary flags (including -pthread which is essential for linking the Pthreads library), and rules to build the executable and clean the project directory.

Code Snippets

Now, let’s write the C code for our producer-consumer application. This code demonstrates the full lifecycle: initialization, locking, waiting, signaling, and destruction of synchronization primitives.

producer_consumer.c

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

#define BUFFER_SIZE 10 // The size of our shared buffer
#define NUM_ITEMS 50   // The total number of items to produce

// Shared resources
int buffer[BUFFER_SIZE];
int count = 0; // Number of items in the buffer
int in = 0;    // Index for producer to write to
int out = 0;   // Index for consumer to read from

// Pthreads synchronization primitives
pthread_mutex_t mutex;
pthread_cond_t cond_full;  // Condition variable for when the buffer is full
pthread_cond_t cond_empty; // Condition variable for when the buffer is empty

// Producer thread function
void* producer(void* arg) {
    int item;
    for (int i = 0; i < NUM_ITEMS; i++) {
        item = i; // The item is just the loop counter

        // Lock the mutex before accessing shared resources
        pthread_mutex_lock(&mutex);

        // Wait while the buffer is full
        while (count == BUFFER_SIZE) {
            printf("Producer: Buffer is FULL. Waiting...\n");
            pthread_cond_wait(&cond_full, &mutex);
        }

        // Add the item to the buffer
        buffer[in] = item;
        in = (in + 1) % BUFFER_SIZE;
        count++;

        printf("Producer: Produced item %d, count = %d\n", item, count);

        // Signal the consumer that the buffer is no longer empty
        pthread_cond_signal(&cond_empty);

        // Unlock the mutex
        pthread_mutex_unlock(&mutex);

        // Simulate some work
        usleep(50000); // 50ms
    }
    return NULL;
}

// Consumer thread function
void* consumer(void* arg) {
    int item;
    for (int i = 0; i < NUM_ITEMS; i++) {
        // Lock the mutex before accessing shared resources
        pthread_mutex_lock(&mutex);

        // Wait while the buffer is empty
        while (count == 0) {
            printf("Consumer: Buffer is EMPTY. Waiting...\n");
            pthread_cond_wait(&cond_empty, &mutex);
        }

        // Remove an item from the buffer
        item = buffer[out];
        out = (out + 1) % BUFFER_SIZE;
        count--;

        printf("Consumer: Consumed item %d, count = %d\n", item, count);

        // Signal the producer that the buffer is no longer full
        pthread_cond_signal(&cond_full);

        // Unlock the mutex
        pthread_mutex_unlock(&mutex);

        // Simulate processing the item
        usleep(100000); // 100ms
    }
    return NULL;
}

int main() {
    pthread_t prod_thread, cons_thread;

    // Initialize mutex and condition variables
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond_full, NULL);
    pthread_cond_init(&cond_empty, NULL);

    printf("Starting producer and consumer threads...\n");

    // Create the producer and consumer threads
    if (pthread_create(&prod_thread, NULL, producer, NULL) != 0) {
        perror("Failed to create producer thread");
        return 1;
    }
    if (pthread_create(&cons_thread, NULL, consumer, NULL) != 0) {
        perror("Failed to create consumer thread");
        return 1;
    }

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

    printf("Threads finished. Cleaning up.\n");

    // Destroy mutex and condition variables
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond_full);
    pthread_cond_destroy(&cond_empty);

    return 0;
}

Code Explanation:

  • Shared Data: The buffercountin, and out variables are global and shared between the threads. The mutex is used to protect all of them.
  • Condition Variables: We use two distinct condition variables. cond_full is used by the producer to wait when the buffer is full. cond_empty is used by the consumer to wait when the buffer is empty.
  • Producer Logic: The producer locks the mutex. It then enters a while loop to check if the buffer is full (count == BUFFER_SIZE). If it is, it calls pthread_cond_wait(&cond_full, &mutex), which atomically releases the mutex and puts the producer to sleep. When it’s woken up (by the consumer signaling cond_full), it re-acquires the mutex and checks the condition again. If the buffer has space, it adds an item, increments the count, and crucially, calls pthread_cond_signal(&cond_empty) to wake up the consumer if it was waiting.
  • Consumer Logic: The consumer’s logic is symmetric. It locks the mutex and waits in a while loop if the buffer is empty (count == 0), waiting on cond_empty. When woken, it consumes an item, decrements the count, and signals the producer via pthread_cond_signal(&cond_full) in case the producer was waiting for space to become available.
  • Main Function: The main function initializes the mutex and both condition variables, creates the two threads, waits for them to complete their work using pthread_join(), and finally destroys the synchronization primitives to free up system resources.

Build, Flash, and Boot Procedures

Since we are developing directly on the Raspberry Pi 5, there is no “flashing” or “booting” procedure in the sense of cross-compilation. We will compile and run the code directly.

1. Navigate to the project directory:

Bash
cd producer-consumer

2. Build the executable using the Makefile:

Bash
make


This command will execute the build rule in the Makefile, running gcc -g -Wall -pthread -o producer_consumer producer_consumer.c.

3. Run the application:

Bash
./producer_consumer

Expected Output

The output will be an interleaved sequence of messages from the producer and consumer. The exact order will vary slightly on each run due to thread scheduling, but the overall pattern will be consistent. You will see the producer fill the buffer, then wait when it becomes full. The consumer will then start consuming items, and once it creates space, the producer will wake up and continue.

Plaintext
Starting producer and consumer threads...
Producer: Produced item 0, count = 1
Producer: Produced item 1, count = 2
...
Producer: Produced item 9, count = 10
Producer: Buffer is FULL. Waiting...
Consumer: Consumed item 0, count = 9
Consumer: Consumed item 1, count = 8
Producer: Produced item 10, count = 10
Producer: Buffer is FULL. Waiting...
Consumer: Consumed item 2, count = 9
...
Consumer: Consumed item 49, count = 0
Consumer: Buffer is EMPTY. Waiting...
Threads finished. Cleaning up.

This output clearly demonstrates the synchronization. The producer fills the buffer up to its capacity (count = 10), then blocks. The consumer, which runs a bit slower due to its longer usleep call, starts its work. As soon as the consumer consumes an item (e.g., item 0), it signals the producer, which wakes up, produces one more item (item 10), and immediately goes back to sleep because the buffer is full again. This dance continues until all 50 items have been produced and consumed. The final “Buffer is EMPTY” message shows the consumer waiting for an item that will never arrive, but since the producer has already finished its loop and exited, the program terminates correctly when pthread_join() returns.

Common Mistakes & Troubleshooting

Condition variables are powerful, but their complexity can lead to subtle bugs that are difficult to diagnose. Here are some of the most common pitfalls and how to avoid them.

Mistake / Issue Symptom(s) Troubleshooting / Solution
Using `if` instead of `while` for the condition check. Program proceeds with incorrect data, leading to crashes, data corruption, or unpredictable behavior after a spurious wakeup. Always wrap the pthread_cond_wait() call in a while loop that re-evaluates the condition. This is non-negotiable.
Lost Wakeup / Signal: Signaling without holding the mutex. Deadlock. A waiting thread misses a signal because it was sent after the condition check but before the thread went to sleep. Always lock the mutex before calling pthread_cond_signal() or pthread_cond_broadcast(). The lock should protect both the data modification and the signal.
Calling `wait` without a lock. Undefined behavior, often leading to an immediate crash or unpredictable errors from the Pthreads library. The mutex passed to pthread_cond_wait() must be locked by the calling thread. Ensure pthread_mutex_lock() is called before the wait loop.
Signaling the wrong condition variable. Deadlock. The producer signals cond_full when the consumer is waiting on cond_empty. The intended recipient never wakes up. Use descriptive names for condition variables (e.g., can_produce, can_consume). Double-check that the thread changing a state signals the correct corresponding condition.
Using `signal` instead of `broadcast`. Thread starvation or logical errors. A state change could satisfy multiple waiters, but only one is woken up, leaving the others stuck. If a signal should unblock all waiting threads (e.g., at a barrier), you must use pthread_cond_broadcast(). Use signal only when any single waiter can handle the task.
Resource Leaks: Forgetting to destroy primitives. Memory and resource leaks. In long-running applications, this can degrade system performance or cause eventual failure. Ensure a clean shutdown path. Call pthread_join() for all threads, then call pthread_mutex_destroy() and pthread_cond_destroy() for every initialized primitive.

Exercises

  1. Modify Buffer and Item Counts:
    • Objective: Understand how changing buffer size and workload affects the program’s behavior.
    • Task: In the producer_consumer.c example, change BUFFER_SIZE to 5 and NUM_ITEMS to 100. Recompile and run the program.
    • Verification: Observe the output. You should see the “Producer: Buffer is FULL. Waiting…” message appear more frequently, as the smaller buffer fills up faster. The program should still run to completion without deadlocking.
  2. Multiple Consumers:
    • Objective: Adapt the program to a multi-consumer scenario.
    • Task: Modify main() to create two consumer threads instead of one. Both consumer threads should run the same consumer function. You will need to adjust the NUM_ITEMS check in the consumer to ensure the program terminates correctly. For example, you could use a shared atomic counter or have both consumers run until a global “done” flag is set by the producer. A simpler approach for this exercise is to have each consumer try to consume NUM_ITEMS / 2 items.
    • Verification: Run the program. You should see output from both consumers, interleaved, as they compete to take items from the buffer. The total number of consumed items should equal the number produced.
  3. Implement a Thread Barrier:
    • Objective: Use pthread_cond_broadcast() to synchronize multiple threads at a specific point.
    • Task: Write a new program where you create 5 threads. Each thread does some “work” (e.g., sleep for a random duration) and then must wait at a barrier. The last thread to arrive at the barrier should signal all other threads to proceed. You will need a global counter, a mutex, and a condition variable. When a thread arrives, it increments the counter. If the count is less than 5, it waits on the condition variable. If the count is 5, it calls pthread_cond_broadcast().
    • Verification: The output should show all 5 threads starting their work, then printing a “waiting at barrier” message. Finally, after the last thread arrives, all 5 threads should print a “passed barrier” message at roughly the same time.
  4. Lost Wakeup Debugging Challenge:
    • Objective: Identify and fix a “lost wakeup” race condition.
    • Task: You are given the following broken code snippet. The mutex lock is in the wrong place in the signaling thread. Integrate this logic into a runnable program and demonstrate the deadlock. Then, fix it.
    // Signaling thread (incorrect) // ... modify data that makes the condition true ... pthread_mutex_unlock(&mutex); pthread_cond_signal(&cond); // Waiting thread (correct) pthread_mutex_lock(&mutex); while (condition_is_false) { pthread_cond_wait(&cond, &mutex); } pthread_mutex_unlock(&mutex);
    • Verification: The broken program should frequently (but not always, as it’s a race condition) hang and never terminate. The fixed program should always run to completion. Explain exactly why the original code fails and why the fix works.
  5. Timed Wait:
    • Objective: Learn to use pthread_cond_timedwait() to prevent indefinite waiting.
    • Task: Modify the consumer in the original example. If the buffer is empty, it should only wait for a maximum of 2 seconds. If it times out, it should print a “Consumer timed out” message and check again. The pthread_cond_timedwait() function requires an absolute time for the timeout, which you’ll need to calculate using clock_gettime().
    • Verification: Run the program. At the very end, after the producer is finished and the buffer is empty, the consumer should print its timeout message repeatedly until its main loop also finishes.

Summary

  • Purpose of Condition Variables: Condition variables provide a mechanism for threads to signal each other and wait for specific conditions to become true, avoiding inefficient CPU-burning busy-waiting.
  • Cooperation with Mutexes: A condition variable is always used with a mutex. The mutex protects the shared data that constitutes the “condition,” while the condition variable handles the waiting and signaling.
  • The pthread_cond_wait Loop: The function pthread_cond_wait() must always be called inside a while loop that checks the condition. This is mandatory to handle spurious wakeups and ensure program correctness.
  • Atomic Wait: pthread_cond_wait() atomically unlocks the associated mutex and puts the thread to sleep. Upon waking, it atomically re-acquires the mutex before returning.
  • Signaling: pthread_cond_signal() wakes at least one waiting thread, while pthread_cond_broadcast() wakes all waiting threads. The choice depends on whether one or many threads can act on the state change.
  • Resource Management: Condition variables, like mutexes, must be initialized before use (pthread_cond_init) and destroyed after use (pthread_cond_destroy) to prevent resource leaks.

Further Reading

  1. The Open Group Base Specifications (POSIX.1-2017): The official documentation for Pthreads functions.
  2. Linux man-pages: Excellent, practical documentation often installed on your development system.
    • man pthread_cond_wait
  3. Butenhof, David R. Programming with POSIX Threads. Addison-Wesley, 1997.
    • The classic, authoritative book on Pthreads. While dated, the fundamental concepts and API remain unchanged and are explained with exceptional clarity.
  4. Downey, Allen B. The Little Book of Semaphores (2nd ed.). Green Tea Press, 2016.
    • While focused on semaphores, this book provides excellent explanations of classic synchronization problems (like producer-consumer and barriers) that are directly applicable to condition variables. Available for free online.
  5. “Spurious wakeups” by Douglas C. Schmidt and Irfan Pyarali
    • An in-depth article that explores the reasons behind spurious wakeups in multithreaded systems. A good read for understanding the “why” behind the while loop rule.

Leave a Comment

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

Scroll to Top