C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
137: Condition Variables in C11
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_tcondition 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
whileloop to guard thecnd_waitcall 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_tandcnd_tobjects are properly initialized and destroyed using the C11<threads.h>library.
- Use a shared buffer (a simple integer variable) and a
There are no comments for now.