Skip to Content
Course content

137: Condition Variables in C11

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Up to this point, we've spent a lot of time talking about mtx_t and how to keep threads from stepping on each other's toes. But there is a fundamental difference between protecting data and waiting for data. If you've ever found yourself writing a loop that just checks a flag over and over again to see if some work is ready, you've hit the wall where mutexes alone aren't enough. You need condition variables.

The CPU-Burning Polling Loop

Let's say we're building a simple log-processing system. We have one thread gathering logs from a network socket and another thread that writes those logs to a file. The naive way to handle this is to use a shared queue and a boolean flag called has_data. The writer thread would look something like this:

while (1) {
    mtx_lock(&log_mutex);
    if (has_data) {
        process_logs();
        has_data = false;
        mtx_unlock(&log_mutex);
    } else {
        mtx_unlock(&log_mutex);
        // Just loop back and try again immediately
    }
}

I've seen this in production code more often than I'd like to admit. On the surface, it works. But in reality, you're just burning CPU cycles. This thread is effectively screaming "Are we there yet?" a billion times a second. If you try to fix this by adding a thrd_sleep inside the else block, you've just traded a CPU problem for a latency problem. Now, if data arrives right after the sleep starts, your program sits idle for several milliseconds for no reason. It's a lose-lose situation.

Sleeping Until the Signal

This is where cnd_t comes in. A condition variable allows a thread to go to sleep—completely off the CPU—and stay that way until another thread explicitly wakes it up. The magic of cnd_wait is that it atomically unlocks the mutex and puts the thread to sleep. When it's woken up, it automatically re-acquires the mutex before returning. This prevents the classic race condition where a signal is sent between the time you check the flag and the time you go to sleep.

Here is how we'd actually implement that log writer using C11 condition variables:

cnd_t data_cond;
mtx_t log_mutex;

// In the writer thread:
while (1) {
    mtx_lock(&log_mutex);
    while (!has_data) {
        cnd_wait(&data_cond, &log_mutex);
    }
    process_logs();
    has_data = false;
    mtx_unlock(&log_mutex);
}

// In the producer thread:
mtx_lock(&log_mutex);
add_logs_to_queue();
has_data = true;
cnd_signal(&data_cond); 
mtx_unlock(&log_mutex);

The "Spurious Wakeup" Trap

You'll notice I used while (!has_data) instead of if (!has_data). This is a critical detail that catches almost everyone the first time they use condition variables. In the world of multi-threading, "spurious wakeups" can happen. For reasons buried deep in the OS kernel or the hardware architecture, a thread might wake up even if no one called cnd_signal.

If you use an if statement, a spurious wakeup would lead your thread to call process_logs() when the queue is actually empty, likely causing a crash or memory corruption. By using a while loop, the thread wakes up, checks the condition, realizes it was a false alarm, and immediately goes back to sleep. I always tell my juniors: never cnd_wait inside an if. Just make it a habit.

Choosing Between Signal and Broadcast

In the example above, I used cnd_signal, which wakes up exactly one waiting thread. This is perfect for a producer-consumer setup because only one thread can process a specific piece of data anyway. However, if you have a "shutdown" signal that needs to tell ten different worker threads to stop what they're doing and exit, cnd_signal won't cut it. You'd use cnd_broadcast instead, which wakes up every single thread currently waiting on that variable. It's a heavier operation, but it's the only way to ensure everyone gets the memo.




📋 Practical Task

Exercise: Implementing a Thread-Safe Message Dispatcher

Build a small program that implements a "Message Dispatcher." You will need one producer thread that generates a sequence of integers (messages) and three consumer threads that wait for these messages.

  • Requirements:
    • Use a shared buffer (a simple integer variable) and a cnd_t condition variable.
    • The producer should push a value, signal the condition variable, and then wait for a consumer to "acknowledge" the message (using a second condition variable) before pushing the next one.
    • Consumers must use a while loop to guard the cnd_wait call to handle spurious wakeups.
    • Each message should be processed by exactly one consumer. The consumer should print which thread ID handled which message value.
    • Ensure all mtx_t and cnd_t objects are properly initialized and destroyed using the C11 <threads.h> library.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.