Condition variables are one of the “three pillars” of C++ multithreading synchronization (mutexes, condition variables, and atomics). They act like a “Schrödinger’s notifier”: you think the notification has been sent, but it may have sunk without a trace; you think no notification was sent, but it may suddenly wake you up, leaving you in vain joy; or you think the lock is secure, but it may quietly leave you with a deadlock pit.
Congratulations, you have likely fallen into the “classic traps” of condition variables.
Today, we will discuss the maddening pitfalls of condition variables and how to avoid them.
Part 1Basic Concepts of Condition Variables
Before discussing errors, let’s clarify a core question: what exactly is a condition variable for?
Let’s use a life scenario analogy (a joke programmers understand about the break room):
-
Mutex (std::mutex): The door to the break room, allowing only one person in at a time, ensuring “mutual exclusion” (like grabbing coffee without fighting);
-
Shared conditions (like “coffee is ready” or “the break room is empty”): The “trigger conditions” for threads to wait;
-
Condition variable (std::condition_variable): The company’s loudspeaker, responsible for announcing “coffee is ready” (notify_one) or “everyone come for benefits” (notify_all), waking up waiting threads.
Essentially, the core function of a condition variable is to “put threads to sleep when a condition is not met, avoiding CPU wastage; and wake them up when the condition is met, allowing them to continue working efficiently” — this saves far more resources than thread polling (imagine: you don’t have to check the break room every 10 seconds to see if the coffee is ready; you just sit and wait for the announcement).
However, there is a key premise:condition variables do not store “notifications” themselves, nor do they guarantee that “notifications are always received” — this is the root of all pitfalls! Just like a broadcast that is forgotten after being announced, those who did not hear it will never know, and those who did may have “misheard” (false wake-up).
Part 2Common Mistakes in Using Condition Variables
Next, we enter the “pitfall record” segment! Each of the following mistakes comes from real project “bloody lessons”. How many have you experienced?
2.1. Lost Wake-up: “The broadcast called too early, the audience hasn’t arrived yet”
Error Scenario:
The producer thread calls notify_one() first, and the consumer thread calls wait() later — as a result, the consumer never receives the notification and is stuck indefinitely.
Error Code Example:
#include <mutex>#include <condition_variable>#include <thread>#include <iostream>std::mutex mtx;std::condition_variable cv;bool coffee_ready = false; // Condition: Is the coffee ready?// Producer: Brew coffee first, then notify (but the consumer hasn't started waiting yet)void producer() { std::lock_guard<std::mutex> lock(mtx); coffee_ready = true; // Coffee is ready cv.notify_one(); // Announce "coffee is ready" std::cout << "Producer: Notification sent, off work!" << std::endl;}// Consumer: Slacking off first, then starts waiting for coffeevoid consumer() { std::this_thread::sleep_for(std::chrono::seconds(1)); // Slacking off for 1 second std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [](){ return coffee_ready; }); // Wait for notification std::cout << "Consumer: Finally got to drink coffee!" << std::endl;}int main() { std::thread t_prod(producer); std::thread t_cons(consumer); t_prod.join(); t_cons.join(); // The consumer is forever stuck here, the program will not end! return 0;}
Why is this wrong?
The notify of a condition variable is “immediate” — it will only be “captured” when the thread is currently in wait(). If notify is called before wait, that notification will “evaporate”, and subsequent waits will block indefinitely.
It’s like your friend shouting “Dinner is ready” in the group at 10 o’clock, and you only enter the group at 10:01, so you naturally won’t see that message and can only wait indefinitely.
Correct Approach:
You must ensure that *wait() is executed beforenotify()** or use a “status variable + lock” as a fallback — because wait() essentially checks the condition → if not met, it sleeps, and the condition is a shared variable protected by a lock (like coffee_ready).
The corrected consumer logic (actually, the code doesn’t need to change, just adjust the thread startup order, let the consumer wait first, and the producer notify later):
int main() { std::thread t_cons(consumer); // Start the consumer first, let it start waiting std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Ensure the consumer enters wait std::thread t_prod(producer); // Then start the producer to send notification t_prod.join(); t_cons.join(); // Normal output: Consumer drinks coffee return 0;}
Core Principle: notify is just a “wake-up signal”; the real “condition state” must exist in the shared variable (protected by a lock). wait will first check the state, then decide whether to sleep — even if the notification is lost, as long as the state is true, wait will return immediately.
2.2. Spurious Wake-up: “The broadcast called incorrectly, and you ran over”
Error Scenario:
Using if instead of while to check the condition, resulting in the thread being “spurious awakened” and skipping the condition check, leading to data errors.
Error Code Example:
// Consumer thread (incorrect version)void consumer() { std::unique_lock<std::mutex> lock(mtx); if (!coffee_ready) { // Checking condition with if (big pit!) cv.wait(lock); // Sleep waiting for notification } // After spurious wake-up, directly execute here, regardless of whether coffee_ready is true std::cout << "Consumer: Drinking... an empty cup?" << std::endl;}
Why is this wrong?
“Spurious wake-up” is the operating system’s “fault” — due to thread scheduling, signal interruptions, etc., cv.wait() may wake up without receiving notify (for example, if the system scheduler “twitches”).
It’s like you are waiting for coffee, and the cleaning lady accidentally bumps the loudspeaker while mopping, causing a noise, and you think “the coffee is ready”. You run over only to find it empty — this is spurious wake-up.
If you check the condition with if, after being spurious awakened, the thread will directly skip the check and drink from an empty cup; whereas if you use while, it will recheck the condition: “Oh, the coffee is not ready, continue sleeping”.
Correct Approach:
Always use *while loops to check conditions, not if**! The second parameter of std::condition_variable::wait() is essentially a while loop, so it is recommended to use the predicate form directly:
// Consumer thread (correct version)void consumer() { std::unique_lock<std::mutex> lock(mtx); // Equivalent to: while (!coffee_ready) { cv.wait(lock); } cv.wait(lock, [](){ return coffee_ready; }); std::cout << "Consumer: Finally got real coffee!" << std::endl;}
2.3. Condition Check and Lock Coordination Error: “Looking at the coffee without locking the door, and then the door gets locked by someone else”
Error Scenario:
Checking the condition after unlocking, or checking the condition without holding the lock — leading to the condition being tampered with by other threads, resulting in data races.
Error Code Example:
void consumer() { std::unique_lock<std::mutex> lock(mtx); lock.unlock(); // Unlocking early (thinking of "improving efficiency", but actually digging a pit) if (!coffee_ready) { // Checking condition while the lock is already released! lock.lock(); cv.wait(lock); }}
Why is this wrong?
coffee_ready is a shared variable and must be checked while holding the lock — otherwise, in the gap of “unlock → check condition → relock → wait”, the producer may have already modified coffee_ready, causing the condition check to fail.
It’s like you first open the break room door (unlock), then stand outside looking at whether the coffee is ready (check condition), and at that moment, someone rushes in and drinks the coffee (modifying coffee_ready). You then enter (relock) and find the coffee is gone, but you have already missed the wait opportunity.
Correct Approach:
Condition checks, wait calls, and condition modifications must all be done while holding the lock! wait() will automatically release the lock while sleeping and reacquire it upon waking, so there is no need to unlock manually:
void consumer() { std::unique_lock<std::mutex> lock(mtx); // Lock cv.wait(lock, [](){ return coffee_ready; }); // Check condition (holding lock) → Sleep (release lock) → Wake up (relock) // Subsequent operations still hold the lock until lock is destructed}
2.4. Confusion in Using Multiple Condition Variables: “One broadcast calls all, everyone runs for nothing”
Error Scenario:
Using the same condition variable to notify multiple different conditions (like using the same broadcast for “coffee is ready” and “the package has arrived”), causing unrelated threads to wake up and waste CPU resources.
Error Code Example:
std::mutex mtx;std::condition_variable cv; // One condition variable for everythingbool coffee_ready = false;bool package_arrived = false;// Consumer 1: waiting for coffeevoid consumer_coffee() { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [](){ return coffee_ready; }); std::cout << "Waiting for coffee: got it!" << std::endl;}// Consumer 2: waiting for packagevoid consumer_package() { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [](){ return package_arrived; }); std::cout << "Waiting for package: got it!" << std::endl;}// Producer: brew coffee and announce (waking both consumers)void producer_coffee() { std::lock_guard<std::mutex> lock(mtx); coffee_ready = true; cv.notify_all(); // Both consumers are awakened}
Why is this wrong?
notify_all() will wake up all threads waiting on this condition variable, but only the thread waiting for coffee will meet the condition, while the thread waiting for the package will be awakened and find the condition unmet, causing it to sleep again — this is the “thundering herd effect”, wasting the overhead of thread wake-up and lock contention.
It’s like the company only has one loudspeaker, and whether it’s “coffee is ready” or “the package has arrived”, it shouts “everyone come quickly”. Both those waiting for coffee and those waiting for the package rush over, only to find it’s not their business, and they can only return to wait, which is highly inefficient.
Correct Approach:
One condition corresponds to one condition variable, avoiding unrelated threads being awakened:
std::mutex mtx;std::condition_variable cv_coffee; // Specifically notify coffeestd::condition_variable cv_package; // Specifically notify packagebool coffee_ready = false;bool package_arrived = false;// Producer brews coffee and only wakes the thread waiting for coffeevoid producer_coffee() { std::lock_guard<std::mutex> lock(mtx); coffee_ready = true; cv_coffee.notify_one(); // Precise wake-up}
2.5. Error in Handling Wait Timeout: “Couldn’t wait any longer and left, but forgot to close the door”
Error Scenario:
Using wait_for() or wait_until() without properly handling the lock after timeout, or misjudging the timeout return value.
Error Code Example:
void consumer() { std::unique_lock<std::mutex> lock(mtx); // Wait for 10 seconds, timeout returns cv.wait_for(lock, std::chrono::seconds(10)); if (coffee_ready) { std::cout << "Got to drink coffee!" << std::endl; } else { std::cout << "Couldn't wait any longer, left!" << std::endl; // Forgot to release the lock? No, lock destructs will release it, but the condition judgment is wrong! }}
Why is this wrong?
-
The return value of wait_for() is std::cv_status::timeout or std::cv_status::no_timeout — timeout does not mean the condition is never met, just that “it was not met within the specified time”;
-
The erroneous code does not check the return value but directly judges coffee_ready — although the lock will be released by lock destruct, the logic may misjudge (for example, after 10 seconds, the coffee may just be ready, but the thread has already exited);
-
A more serious error: if manually locking and timing out, forgetting to unlock (for example, using std::lock_guard will not, but using pthread_mutex_lock may).
Correct Approach:
Check the return value of wait_for() and handle the timeout in conjunction with the condition judgment:
void consumer() { std::unique_lock<std::mutex> lock(mtx); // Wait for 10 seconds while checking the condition auto status = cv.wait_for(lock, std::chrono::seconds(10), [](){ return coffee_ready; }); if (status) { // Condition met (woken by notify) std::cout << "Got to drink coffee!" << std::endl; } else { // Timeout (condition still not met) std::cout << "Waited 10 seconds without coffee, left!" <<< std::endl; } // lock destruct automatically releases the lock, no need for manual handling}
Core Principle: Timeout means “time is up”, not “condition will never be met” — you must distinguish between “timeout” and “woken up” through the return value, and then decide on subsequent logic.
Part 3Correct Usage Patterns for Condition Variables
After avoiding the pitfalls, let’s learn a few “universal templates” — the following patterns cover 90% of multithreading synchronization scenarios, just copy and use!
3.1. Producer-Consumer Pattern (Classic of Classics)
Core Requirement: The producer puts data into the queue, and the consumer takes data from the queue; the producer waits when the queue is full, and the consumer waits when the queue is empty.
Correct Code Example:
#include <mutex>#include <condition_variable>#include <thread>#include <queue>#include <iostream>#include <chrono>const int MAX_QUEUE_SIZE = 5; // Maximum queue capacitystd::queue<int> data_queue; // Shared queuestd::mutex mtx;std::condition_variable cv_not_full; // Queue not full (producer waits)std::condition_variable cv_not_empty; // Queue not empty (consumer waits)// Producer: Produce data, wait if the queue is fullvoid producer(int id) { for (int i = 0; i < 10; ++i) { std::unique_lock<std::mutex> lock(mtx); // Queue is full, wait for "queue not full" notification cv_not_full.wait(lock, [](){ return data_queue.size() < MAX_QUEUE_SIZE; }); // Produce data int data = id * 100 + i; data_queue.push(data); std::cout << "Producer" << id << ": Produced " << data << ", queue size: " << data_queue.size() << std::endl; lock.unlock(); // Unlock early (optional, optimize performance) cv_not_empty.notify_one(); // Notify consumer "queue not empty" std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulate production time }}// Consumer: Consume data, wait if the queue is emptyvoid consumer(int id) { while (true) { std::unique_lock<std::mutex> lock(mtx); // Queue is empty, wait for "queue not empty" notification cv_not_empty.wait(lock, [](){ return !data_queue.empty(); }); // Consume data int data = data_queue.front(); data_queue.pop(); std::cout << "Consumer" << id << ": Consumed " << data << ", queue size: " << data_queue.size() << std::endl; lock.unlock(); // Unlock early (optional) cv_not_full.notify_one(); // Notify producer "queue not full" std::this_thread::sleep_for(std::chrono::milliseconds(200)); // Simulate consumption time }}int main() { std::thread prod1(producer, 1); std::thread prod2(producer, 2); std::thread cons1(consumer, 1); std::thread cons2(consumer, 2); prod1.join(); prod2.join(); cons1.join(); // In actual projects, exit conditions need to be set, here simplified to infinite loop cons2.join(); return 0;}
Key Points:
-
Two condition variables: cv_not_full (for producer waiting) and cv_not_empty (for consumer waiting), precise wake-up;
-
Use while (predicate) to check conditions, avoiding spurious wake-ups;
-
Timely notifications after production/consumption to avoid deadlocks;
-
Queue operations are protected by locks throughout, avoiding data races.
3.2. Barrier Synchronization Pattern (Waiting for All Threads to “Gather”)
Core Requirement: Multiple threads wait until all threads complete a certain phase before entering the next phase together (for example, “all threads complete initialization before starting work”).
Correct Code Example:
#include <mutex>#include <condition_variable>#include <thread>#include <iostream>#include <vector>std::mutex mtx;std::condition_variable cv;int ready_count = 0; // Number of threads that have completed initializationconst int TOTAL_THREADS = 3; // Total number of threads// Thread task: Initialize → Wait for all threads → Start workingvoid thread_task(int id) { // Simulate initialization time std::this_thread::sleep_for(std::chrono::milliseconds(id * 100)); std::cout << "Thread" << id << ": Initialization complete!" << std::endl; std::unique_lock<std::mutex> lock(mtx); ready_count++; // Have all threads completed initialization? if (ready_count == TOTAL_THREADS) { cv.notify_all(); // Notify all waiting threads std::cout << "Thread" << id << ": Everyone is here, let's go!" << std::endl; } else { // Wait for other threads to complete initialization cv.wait(lock, [](){ return ready_count == TOTAL_THREADS; }); std::cout << "Thread" << id << ": Received notification, starting work!" << std::endl; }}int main() { std::vector<std::thread> threads; for (int i = 0; i < TOTAL_THREADS; ++i) { threads.emplace_back(thread_task, i); } for (auto& t : threads) { t.join(); } return 0;}
Output:
Thread 0: Initialization complete!Thread 1: Initialization complete!Thread 2: Initialization complete!Thread 2: Everyone is here, let's go!Thread 0: Received notification, starting work!Thread 1: Received notification, starting work!
Key Points:
-
Use a counter ready_count to record completion status, protected by a lock;
-
The last thread to complete calls notify_all(), waking all waiting threads;
-
Other threads wait until the counter reaches the total number of threads, avoiding starting work early.
3.3. Event Notification Pattern (Waiting for a “One-time Event”)
Core Requirement: One thread waits for a one-time event (like “data initialization complete” or “task execution finished”), and another thread triggers that event, allowing the waiting thread to continue execution.
Correct Code Example:
#include <mutex>#include <condition_variable>#include <thread>#include <iostream>#include <chrono>std::mutex mtx;std::condition_variable cv;bool event_triggered = false; // Has the event been triggered (one-time)int result = 0; // Event result// Trigger thread: Execute task → Trigger eventvoid trigger_thread() { std::cout << "Trigger thread: Starting task..." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(2)); // Simulate task time result = 42; // Task result std::lock_guard<std::mutex> lock(mtx); event_triggered = true; // Mark event triggered cv.notify_one(); // Notify waiting thread std::cout << "Trigger thread: Task completed, event triggered!" << std::endl;}// Waiting thread: Wait for event → Process resultvoid wait_thread() { std::cout << "Waiting thread: Waiting for event to trigger..." << std::endl; std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [](){ return event_triggered; }); // Wait for event std::cout << "Waiting thread: Received event, result is " << result << "!" << std::endl;}int main() { std::thread t_trigger(trigger_thread); std::thread t_wait(wait_thread); t_trigger.join(); t_wait.join(); return 0;}
Key Points:
-
The event state event_triggered is “one-time” (not modified after being triggered);
-
The waiting thread checks the event state through the wait predicate, avoiding spurious wake-ups;
-
The event result is protected by a lock, ensuring that the waiting thread can only read it after the triggering thread modifies it.
Part 4Advanced Condition Variable Techniques
Having mastered the basic patterns, here are two “advanced techniques” to help you stand out in interviews and projects!
4.1. Interruptible Wait: Adding an “Emergency Brake” to Threads
The C++ standard library’s std::condition_variable does not support directly “interrupting wait” (for example, if the main thread wants to make it exit while it is waiting), but we can implement it ourselves — using a “stop flag” combined with the condition variable’s predicate.
Implementation Code:
#include <mutex>#include <condition_variable>#include <thread>#include <iostream>#include <chrono>std::mutex mtx;std::condition_variable cv;bool stop_flag = false; // Stop flag (interrupt signal)bool coffee_ready = false;// Consumer thread: Wait that can be interruptedvoid consumer() { std::cout << "Consumer: Starting to wait for coffee, can be interrupted at any time..." << std::endl; std::unique_lock<std::mutex> lock(mtx); // Predicate: coffee is ready or stop signal received cv.wait(lock, [](){ return coffee_ready || stop_flag; }); if (stop_flag) { std::cout << "Consumer: Received stop signal, giving up waiting for coffee!" << std::endl; return; } std::cout << "Consumer: Got to drink coffee!" << std::endl;}// Main thread: Send stop signalint main() { std::thread t_cons(consumer); std::this_thread::sleep_for(std::chrono::seconds(1)); // Let the consumer enter waiting // Decide to interrupt waiting { std::lock_guard<std::mutex> lock(mtx); stop_flag = true; } cv.notify_one(); // Wake up the consumer to check the stop flag t_cons.join(); return 0;}
Core Idea:
-
Add a stop_flag as an interrupt signal, protected by a lock;
-
The wait predicate checks both the “target condition” and the “stop flag”;
-
After the main thread sets stop_flag, it calls notify_one() to wake the waiting thread, which wakes up and checks the stop flag, exiting directly.
4.2. Combining Condition Variables with RAII Pattern: Preventing “Forgetting to Unlock”
RAII (Resource Acquisition Is Initialization) is C++’s “black magic” that can automatically manage resources (like releasing locks). We can encapsulate a condition variable utility class to make usage safer and simpler.
Encapsulation Example:
#include <mutex>#include <condition_variable>#include <functional>// RAII-style condition variable utility classclass ConditionVariable {private: std::mutex mtx_; std::condition_variable cv_;public: // Wait: pass in condition predicate template<typename Predicate> void wait(Predicate pred) { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, std::move(pred)); } // Wait with timeout template<typename Predicate, typename Rep, typename Period> bool wait_for(const std::chrono::duration<Rep, Period>& timeout, Predicate pred) { std::unique_lock<std::mutex> lock(mtx_); return cv_.wait_for(lock, timeout, std::move(pred)); } // Wake one thread void notify_one() { cv_.notify_one(); } // Wake all threads void notify_all() { cv_.notify_all(); } // Manually acquire lock (if condition needs modification) std::unique_lock<std::mutex> lock() { return std::unique_lock<std::mutex>(mtx_); }};// Usage Example#include <thread>#include <iostream>ConditionVariable cv;bool coffee_ready = false;void producer() { auto lock = cv.lock(); // Manually lock coffee_ready = true; cv.notify_one(); // Wake up consumer std::cout << "Producer: Coffee is ready!" << std::endl;}void consumer() { cv.wait([](){ return coffee_ready; }); // Automatically lock/unlock std::cout << "Consumer: Got to drink coffee!" << std::endl;}int main() { std::thread t_prod(producer); std::thread t_cons(consumer); t_prod.join(); t_cons.join(); return 0;}
Advantages:
-
Encapsulates mutex and condition_variable, avoiding scattered management;
-
The wait method automatically handles lock acquisition and release, preventing forgetting to unlock;
-
Simple interface, allowing users to focus on business logic without worrying about lock details.
Part 5Testing and Debugging Condition Variables
Is debugging multithreaded code difficult? Master the following techniques to expose bugs in condition variables!
5.1. Unit Testing Pattern: Covering All Extreme Scenarios
Testing condition variables focuses on simulating scenarios like “notification loss”, “spurious wake-up”, “timeout”, and “multithreading competition”. It is recommended to use the Google Test framework.
Testing Example (Pseudocode):
#include <gtest/gtest.h>#include "condition_variable.h" // The class we encapsulatedTEST(ConditionVariableTest, NotifyAfterWait) { // Test: wait first, then notify (normal scenario) ConditionVariable cv; bool ready = false; std::thread t([&](){ cv.wait([&](){ return ready; }); }); // Ensure the thread enters wait std::this_thread::sleep_for(std::chrono::milliseconds(100)); { auto lock = cv.lock(); ready = true; } cv.notify_one(); t.join(); ASSERT_TRUE(ready);}TEST(ConditionVariableTest, LostWakeup) { // Test: notify first, then wait (notification lost, should be covered by condition state) ConditionVariable cv; bool ready = false; // Notify first { auto lock = cv.lock(); ready = true; } cv.notify_one(); // Then wait bool notified = false; std::thread t([&](){ cv.wait([&](){ return ready; }); notified = true; }); t.join(); ASSERT_TRUE(notified); // Even if the notification is lost, wait checks the condition as true, will still return}TEST(ConditionVariableTest, Timeout) { // Test: timeout scenario ConditionVariable cv; bool ready = false; auto start = std::chrono::steady_clock::now(); bool result = cv.wait_for(std::chrono::seconds(1), [&](){ return ready; }); auto end = std::chrono::steady_clock::now(); ASSERT_FALSE(result); // Timeout returns false ASSERT_GE(std::chrono::duration_cast<std::chrono::seconds>(end - start).count(), 1);}
Testing Key Points:
-
Cover “normal wake-up”, “notification loss”, “timeout”, “spurious wake-up” (can simulate by notifying without modifying condition);
-
Check the core logic of the condition variable: whether the condition state is correct, whether threads wake up/sleep as expected.
5.2. Deadlock Detection and Debugging: Identifying the “Lock Competition” Culprit
Deadlocks related to condition variables are mostly caused by “inconsistent lock order” or “not releasing the lock during wait”. The following debugging tools and methods are recommended:
1). GDB Debugging (Linux/Mac):
-
Use the thread command to view all threads;
-
Use thread <id> to switch to the target thread;
-
Use bt to view the thread call stack, checking if it is stuck in wait() or lock();
-
Use info threads to check thread status (__lll_lock_wait indicates waiting for a lock).
2). Logging:
Log before and after acquiring/releasing locks, wait/notify, recording thread ID, timestamp, and condition state:
#define LOG(...) std::cout << "[" << std::this_thread::get_id() << "] " << __VA_ARGS__ << std::endl;// Example: void consumer() { LOG("Consumer: Preparing to lock"); std::unique_lock<std::mutex> lock(mtx); LOG("Consumer: Lock acquired, starting wait"); cv.wait(lock, [](){ LOG("Consumer: Checking condition, coffee_ready=" << coffee_ready); return coffee_ready; }); LOG("Consumer: Woken up, starting consumption");}
3). Deadlock Detection Tools:
-
Linux: valgrind –tool=helgrind ./your_program (detects data races and deadlocks);
-
Windows: Visual Studio’s “Thread Debugging Window” and “Deadlock Detection” features;
-
Cross-platform: ThreadSanitizer (built into Clang/GCC, compile with -fsanitize=thread).
Conclusion
Finally, here’s a mnemonic to help you avoid 99% of the pitfalls:
“Lock protects condition, while checks, notify precisely, timeout must check”
-
Lock protects condition: The shared state of the condition variable (like coffee_ready) must be protected by a mutex;
-
While checks: Always use while (or predicates) to check conditions, avoiding spurious wake-ups;
-
Notify precisely: One condition corresponds to one condition variable, use notify_one instead of notify_all (unless all need to be awakened);
-
Timeout must check: After a timeout, the return value must be checked to avoid misjudging the condition state.
Condition variables are not difficult; the challenge lies in avoiding those “seemingly correct but actually fatal” details.
If this article helps you avoid a pitfall, or if you have other experiences to share, feel free to comment! Like and save it, so you can refer back to it next time you encounter condition variable issues!