Click the “C++ Players, please get ready” button below, select “Follow/Pin/Star the public account” for valuable content and benefits, delivered to you first! Recently, some friends mentioned they did not receive the daily article push, which is due to WeChat changing its push mechanism, causing those who did not star the public account to miss the daily article push and not receive some practical knowledge and information. Therefore, it is recommended that everyone star it ⭐️ so that you can receive the push immediately in the future.

1. Why Use Mutexes?
In the world of C++ multithreaded programming, <span>std::mutex</span> (mutex) is the first synchronization primitive we encounter and the most fundamental one. It ensures that shared resources are accessed by only one thread at any given time, effectively preventing data races. However, merely ensuring mutual access is far from sufficient in many complex concurrent scenarios.
Imagine a classic “producer-consumer” scenario: one or more producer threads place tasks into a shared queue, while one or more consumer threads take tasks from the queue for processing. What should the consumer do if the queue is empty? A naive idea is to have the consumer thread continuously check if the queue is empty in a loop, protected by a mutex.
// Pseudo code - An inefficient waiting method
std::mutex mtx;
std::queue<Task> tasks;
void consumer() {
while (true) {
mtx.lock();
if (!tasks.empty()) {
// ... process task ...
mtx.unlock();
break;
}
mtx.unlock();
// Yield CPU time slice, but will quickly wake up to check again
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
This method is known as “polling” or “busy-waiting.” Its problems are evident: the consumer thread frequently wakes up, locks, checks, and unlocks even when there is nothing to do, which wastes CPU resources and can degrade overall system performance due to frequent lock contention.
This is precisely the limitation of <span>std::mutex</span>: it solves the “mutual exclusion” problem but cannot elegantly handle the “wait-notify” problem between threads. We need a mechanism that allows the consumer thread to efficiently “suspend” and enter a sleep state when the queue is empty, until the producer places a new task and then “wakes” it up.
<span>std::condition_variable</span> (condition variable) was born for this purpose. It is a more advanced synchronization primitive designed specifically to solve the “wait-notify” scenario, allowing threads to efficiently block and wait when specific conditions are not met, and to be awakened by other threads when conditions are satisfied, thus building efficient and clear concurrent logic.
2. Core Concept: What is std::condition_variable?
Definition: <span>std::condition_variable</span> is a synchronization primitive that allows one or more threads to block themselves until they receive a notification from another thread and a specific condition (Predicate) is met. It does not contain any condition information itself but works in conjunction with a mutex and a condition (usually some state of shared data) that needs to be checked.
Workflow Analogy: A Clinic with a Waiting Room
To better understand its working mode, we can use a real-life example for analogy:
- Shared Data: Like the medical record book in the clinic, which needs to be read and modified by doctors (threads).
- Mutex (
<span>std::mutex</span>): The door lock of the clinic. To ensure that the medical record is handled by only one doctor at any time, the doctor must lock the door before entering and unlock it when leaving. This ensures mutual access to the medical record book. - Condition Variable (
<span>std::condition_variable</span>): The calling system in the waiting room. - Waiting Thread: Like a patient waiting for their turn in the waiting room. If the patient finds that there is a doctor in the clinic (the lock is occupied) or their turn has not come yet (the condition is not met), they will not pace back and forth at the door (busy waiting), but will return to their seat in the waiting room to rest (the thread enters a sleep state), consuming no energy (CPU resources).
- Notifying Thread: Like the nurse in the clinic. When the doctor finishes with the previous patient (the shared data has been modified, and the condition may have been met), the nurse will press the button on the calling system (condition variable) to notify the next patient that they can see the doctor.
- Condition/Predicate: Refers to the specific condition that the patient needs to wait for, such as “it’s my turn now.” Even if the patient is awakened by the calling system, they need to confirm again that the number displayed on the screen is indeed theirs before entering the clinic.
This analogy clearly illustrates the core workflow of <span>std::condition_variable</span><span>: threads protect shared data with a mutex and then use a condition variable to wait for a condition to be met, thus avoiding unnecessary CPU consumption.</span>
3. Key APIs and Working Principle Analysis
<span>std::condition_variable</span> has a very simple core API, mainly consisting of three operations: <span>wait()</span>, <span>notify_one()</span>, and <span>notify_all()</span>.
<span>wait()</span>
<span>wait()</span> is the most complex and core function of the condition variable. It has two overloaded versions:
// Version 1: Wait without condition
void wait(std::unique_lock<std::mutex>& lock);
// Version 2: Wait with predicate
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);
<span>wait</span>‘s working process can be described as an atomic trilogy:
- Release the lock (
<span>lock.unlock()</span>): When a thread calls<span>wait</span>, it immediately releases the mutex held by the passed-in<span>std::unique_lock</span>. This is crucial because it allows other threads (like producers) to acquire the lock, modify shared data, and meet the waiting condition. - Block the thread: After releasing the lock, the thread immediately enters a blocked (or sleep) state, waiting for notifications from other threads. At this point, it no longer occupies CPU time.
- Wake up and re-acquire the lock (
<span>lock.lock()</span>): When the thread is awakened by<span>notify_one()</span><span> or </span><code><span>notify_all()</span><span>, the </span><code><span>wait</span>function does not return immediately. It first attempts to re-acquire the mutex that was previously released. Once it successfully acquires the lock, the<span>wait</span>function will officially return.
Why must it be used with <span>std::unique_lock</span>?<span>std::condition_variable::wait</span> requires the use of <span>std::unique_lock</span> instead of <span>std::lock_guard</span>. This is because the internal mechanism of <span>wait</span> needs to release the lock during the wait and re-acquire it when awakened. <span>std::unique_lock</span> provides flexible <span>lock()</span> and <span>unlock()</span><code><span> methods that can meet this dynamic lock management requirement, while </span><code><span>std::lock_guard</span> only supports locking at construction and unlocking at destruction in the RAII pattern, which cannot meet the needs of <span>wait</span>.
<span>notify_one()</span>
Wakes up one thread that is currently <span>wait</span>ing. If no threads are currently waiting, calling <span>notify_one()</span><span> does nothing. If multiple threads are waiting, the standard only guarantees that at least one will be awakened, but which one is determined by the operating system and thread library's scheduling policy, usually the one that has been waiting the longest.</span>
Applicable Scenarios: When you are sure that only one thread needs to respond to a state change, or when all waiting threads perform homogeneous tasks, waking up any one of them will suffice (for example, multiple consumer threads processing a task queue), <span>notify_one()</span> is the most efficient choice.
<span>notify_all()</span>
Wakes up all threads that are currently <span>wait</span>ing. Similarly, if no threads are waiting, calling it is also ineffective.
Applicable Scenarios:
- When a change in the state of shared data may satisfy the conditions of multiple waiting threads. For example, a broadcast event that needs to wake up all listeners to handle.
- When waiting threads may be waiting for different conditions, and a state change may satisfy several of them simultaneously.
- When it is difficult to determine which specific thread should be awakened, waking all is a simple and safe choice, although it may incur some performance overhead.
Analyzing “Spurious Wakeup”
This is a key concept that must be understood and correctly handled when using condition variables.
What is a spurious wakeup? A thread that is currently <span>wait()</span>ing may be “unexpectedly” awakened without any thread calling <span>notify_one()</span> or <span>notify_all()</span><span>. This is not a bug in the C++ standard library but a real phenomenon in operating system thread scheduling, which may occur for performance optimization or other low-level reasons.</span>
How to cope?<span>wait()</span>‘s correct usage is to always place it within a loop and check the waiting condition inside the loop.
// Incorrect usage
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock); // If a spurious wakeup occurs, the thread will continue executing, but the condition may still not be met
// Correct usage
std::unique_lock<std::mutex> lock(mtx);
while (!condition_is_met()) { // Must wrap with a loop
cv.wait(lock);
}
This is precisely the value of the second overloaded version of <span>wait(lock, predicate)</span><span>. It is equivalent to:</span>
// Internal logic of the second overloaded version of wait
while (!predicate()) {
cv.wait(lock);
}
Using the predicate version of <span>wait</span> is the preferred best practice because it is more concise, less error-prone, and clearly expresses the intent of waiting.
<span>cv.wait(lock, [&]{ return !tasks.empty(); });</span>
This line of code perfectly handles spurious wakeups:
- When
<span>wait</span>is called, it first checks the lambda expression<span>[&]{ return !tasks.empty(); }</span>. - If it returns
<span>true</span>(the condition is met),<span>wait</span>returns immediately, and the thread continues executing. - If it returns
<span>false</span>(the condition is not met), the thread releases the lock and enters the waiting state. - When the thread is awakened (whether by normal notification or spurious wakeup), it will re-acquire the lock and check the lambda expression again. Only when the lambda returns
<span>true</span>will<span>wait</span>finally return.
4. Classic Practice: Producer-Consumer Model
Below is a complete, compilable, and runnable C++ code example that implements a thread-safe task queue, perfectly demonstrating the application of <span>std::condition_variable</span><span>.</span>
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <chrono>
class TaskQueue {
public:
void push(int task) {
// 1. Acquire lock to protect shared data
std::unique_lock<std::mutex> lock(mtx_);
// 2. Add task to the queue
tasks_.push(task);
std::cout << "Producer pushed task: " << task << std::endl;
// 3. Lock is automatically released when lock is destructed
// lock.unlock(); // Can also manually unlock, but RAII is better
// 4. Notify one waiting consumer thread
// At this point, even if the lock has not been fully released, the awakened thread must wait for the lock to be released to continue
cv_.notify_one();
}
int pop() {
// 1. Acquire lock to protect shared data
std::unique_lock<std::mutex> lock(mtx_);
// 2. Wait for condition to be met: queue is not empty
// wait will atomically:
// a. Check lambda, if false, release lock and block
// b. Upon being awakened, re-acquire lock and check lambda again
// c. If lambda is true, wait returns, and the thread continues holding the lock
cv_.wait(lock, [this] { return !tasks_.empty(); });
// 3. At this point, we are sure the queue is not empty and we hold the lock
int task = tasks_.front();
tasks_.pop();
std::cout << "Consumer popped task: " << task << std::endl;
// 4. Lock is automatically released when lock is destructed
return task;
}
private:
std::queue<int> tasks_;
std::mutex mtx_;
std::condition_variable cv_;
};
void producer_thread(TaskQueue* queue, int start_id) {
for (int i = 0; i < 5; ++i) {
queue->push(start_id + i);
// Simulate production time
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
}
void consumer_thread(TaskQueue* queue, int thread_id) {
for (int i = 0; i < 5; ++i) {
int task = queue->pop();
// Simulate consumption time
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
int main() {
TaskQueue queue;
// Create producer and consumer threads
std::thread producer1(producer_thread, &queue, 100);
std::thread producer2(producer_thread, &queue, 200);
std::thread consumer1(consumer_thread, &queue, 1);
std::thread consumer2(consumer_thread, &queue, 2);
// Wait for all threads to finish
producer1.join();
producer2.join();
consumer1.join();
consumer2.join();
return 0;
}
5. Core Advantages of std::condition_variable
- CPU Efficiency: This is its core value. Compared to busy waiting or polling, waiting threads completely enter a sleep state (blocked), consuming no CPU cycles. They are only awakened by the operating system when notified and participate in scheduling. This is crucial for building high-performance, low-power applications.
- Avoiding Race Conditions: Condition variables inherently solve the race conditions that may arise from separating “check-wait”. The
<span>wait</span>function bundles the steps of “releasing the lock” and “entering wait” into an atomic operation, ensuring that the condition cannot be changed by other threads between the decision to wait and the actual waiting state. - Logical Decoupling: It elegantly separates the logic of the “waiting party” (like consumers) and the “notifying party” (like producers). The waiting party only needs to focus on its waiting conditions, while the notifying party only needs to notify when it changes a state that may affect that condition. This clear division of responsibilities makes the code easier to understand, reason about, and maintain.
6. Common Pitfalls
-
Must always be used with
<span>std::unique_lock</span>: As mentioned earlier, this is a design requirement of the<span>wait</span>function to support its internal unlocking and re-locking operations. -
Must wrap the call to
<span>wait</span>with a loop and condition check: Whether using a direct<span>while</span>loop or the predicate version of<span>wait</span>, this is to correctly handle spurious wakeups and ensure that the waiting condition is indeed satisfied when the thread continues execution. This is the golden rule of using condition variables. -
Do you need to hold the lock when calling
<span>notify_one()</span>/<span>notify_all()</span>?
- Holding the lock when calling (as in example code): This is the most common and safest way. It ensures that the state of shared data is consistent when the notification is sent. The awakened thread will immediately try to acquire the lock but will be blocked until the notifying thread releases the lock.
- Calling without holding the lock:
{ std::unique_lock<std::mutex> lock(mtx_); tasks_.push(task); } // Lock is released here cv_.notify_one(); // Notify outside the lockThis method may have a slight performance advantage in some cases. It releases the lock before sending the notification, reducing the waiting time for the awakened thread to acquire the lock (i.e., lock contention). However, it may also introduce a subtle “hurry up and wait” problem: the awakened consumer thread may run immediately after the producer releases the lock and acquire it, but another producer thread may have already acquired the lock and emptied the queue again, causing the consumer to wake up and find the condition unmet and have to wait again. Although predicate loops can handle this correctly, it increases the uncertainty of thread scheduling.
- Conclusion: Unless there is a clear performance bottleneck analysis indicating the need for optimization, **it is recommended to always call
<span>notify</span>while holding the lock** for simpler and more predictable behavior.
Beware of the “Thundering Herd” effect caused by <span>notify_all()</span>: <span>notify_all()</span> wakes up all waiting threads, but usually only one or a few threads can successfully process the resource (e.g., take a task from the queue). All awakened threads will immediately compete for the same mutex, and most threads will find the condition unmet after acquiring the lock (because the resource has been taken by other threads) and will have to wait again. This phenomenon of many threads being unnecessarily awakened and causing intense lock contention is the “Thundering Herd” effect, which leads to unnecessary context switching and CPU consumption.
- Best Practice: Prefer using
<span>notify_one()</span>. Only use<span>notify_all()</span>in scenarios where it is indeed necessary to wake all threads or when it is impossible to determine which thread should be awakened.
7. Conclusion
<span>std::condition_variable</span> is not an isolated tool but a key component in the C++ concurrency toolbox that complements <span>std::mutex</span>. It provides an efficient “wait-notify” mechanism that perfectly compensates for the shortcomings of mutexes in thread cooperation. Mastering <span>std::condition_variable</span>, especially understanding its interaction with locks, the atomicity of <span>wait</span>, and handling spurious wakeups, is a watershed moment from being able to write multithreaded code to writing efficient and robust concurrent applications. It is the cornerstone for building reactive, event-driven concurrent models and advanced synchronization structures (like semaphores, barriers, etc.), occupying an indispensable position in C++ concurrent programming.
This article is a professional and reliable content formed after rigorous review of relevant authoritative literature and materials. All data in the full text can be traced back. Special statement: Data and materials have been authorized. The content of this article does not involve any biased views and objectively describes the facts themselves with a neutral attitude.