Cpp Libs Condition_Variable
# C++ Standard Library: ``
In multi-threaded programming, synchronization between threads is a critical challenge.
The C++11 standard introduced the `` header, which provides a mechanism allowing threads to suspend execution (block) when certain conditions are not met, and remain suspended until another thread notifies them that the condition has been satisfied.
A `std::condition_variable` is a high-level synchronization primitive. Compared to low-level synchronization techniques (such as busy-waiting or manual polling), it is significantly more efficient, safe, and elegant.
---
## Introduction to Condition Variables
A `std::condition_variable` is a class template used to synchronize threads. It allows one or more threads to wait for a specific condition to become true. Another thread can modify the shared state and notify the waiting thread(s) to wake up and resume execution.
To prevent race conditions, a `std::condition_variable` **always** works in conjunction with a `std::unique_lock`.
---
## Syntax and API Reference
To use condition variables, you must include the `` header:
```cpp
#include
```
### Class Definition Outline
```cpp
namespace std {
class condition_variable {
public:
condition_variable();
~condition_variable();
// Prevent copying and moving
condition_variable(const condition_variable&) = delete;
condition_variable& operator=(const condition_variable&) = delete;
// Waiting methods
void wait(std::unique_lock& lock);
template
void wait(std::unique_lock& lock, Predicate pred);
template
std::cv_status wait_for(std::unique_lock& lock,
const std::chrono::duration& rel_time);
template
bool wait_for(std::unique_lock& lock,
const std::chrono::duration& rel_time,
Predicate pred);
template
std::cv_status wait_until(std::unique_lock& lock,
const std::chrono::time_point& abs_time);
template
bool wait_until(std::unique_lock& lock,
const std::chrono::time_point& abs_time,
Predicate pred);
// Notification methods
void notify_one() noexcept;
void notify_all() noexcept;
};
}
```
### Key Member Functions
* **`wait(unique_lock& lock)`**: Atomically releases the lock and blocks the current thread. When notified, the thread wakes up and re-acquires the lock.
* **`wait(unique_lock& lock, Predicate pred)`**: Equivalent to `while (!pred()) { wait(lock); }`. This overload is highly recommended as it automatically handles **spurious wakeups**.
* **`wait_for`**: Blocks the thread until notified or the specified relative duration has elapsed.
* **`wait_until`**: Blocks the thread until notified or a specific absolute time point is reached.
* **`notify_one()`**: Wakes up one of the threads currently waiting on this condition variable. If no threads are waiting, nothing happens.
* **`notify_all()`**: Wakes up all threads currently waiting on this condition variable.
---
## Code Example: Producer-Consumer Problem
Below is a complete, practical example demonstrating how to implement a classic Producer-Consumer queue using `std::condition_variable`.
```cpp
#include
#include
#include
#include
#include
#include
std::mutex mtx;
std::condition_variable cv;
std::queue product;
void producer(int id) {
for (int i = 0; i < 5; ++i) {
// Acquire the lock before modifying shared resources
std::unique_lock lck(mtx);
int item = id * 100 + i;
product.push(item);
std::cout << "Producer " << id << " produced " << item << std::endl;
// Notify one waiting consumer thread that data is available
cv.notify_one();
// Release the lock before sleeping to let other threads run
lck.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer(int id) {
while (true) {
std::unique_lock lck(mtx);
// Wait until the queue is not empty.
// The lambda function acts as a predicate to protect against spurious wakeups.
cv.wait(lck, []{ return !product.empty(); });
// Process the item
if (!product.empty()) {
int prod = product.front();
product.pop();
std::cout << "Consumer " << id << " consumed " << prod << std::endl;
}
lck.unlock(); // Release lock
}
}
int main() {
std::thread producers;
std::thread consumers;
// Start 2 producer threads
for (int i = 0; i < 2; ++i) {
producers = std::thread(producer, i + 1);
}
// Start 2 consumer threads
for (int i = 0; i < 2; ++i) {
consumers = std::thread(consumer, i + 1);
}
// Join producers (consumers run indefinitely in this simple example)
for (int i = 0; i < 2; ++i) {
producers.join();
}
// Note: In production code, you should implement a clean shutdown mechanism
// for the consumer threads instead of letting them run indefinitely.
for (int i = 0; i < 2; ++i) {
consumers.detach();
}
return 0;
}
```
### Sample Output
The exact output sequence may vary depending on the operating system's thread scheduler, but it will follow a logical order where items are consumed only after they are produced:
```text
Producer 1 produced 100
Producer 2 produced 200
Consumer 1 consumed 100
Producer 1 produced 101
Consumer 2 consumed 200
Producer 2 produced 201
Consumer 1 consumed 101
...
```
---
## Critical Considerations
When working with `std::condition_variable`, keep the following best practices in mind to avoid deadlocks, race conditions, and performance issues:
1. **Always Use `std::unique_lock`**:
The `wait` family of functions requires a `std::unique_lock`. You cannot use `std::lock_guard` because `std::lock_guard` does not support the explicit locking and unlocking operations required during the wait cycle.
2. **Protect Against Spurious Wakeups**:
A waiting thread can be awakened even if no thread has explicitly notified the condition variable (this is called a *spurious wakeup*). Always use the `wait(lock, predicate)` overload, or manually wrap your wait in a loop:
```cpp
while (!condition_is_met) {
cv.wait(lock);
}
```
3. **Lock Acquisition Order**:
The waiting thread must acquire the mutex lock *before* calling `wait`. When `wait` is called, it automatically releases the lock and puts the thread to sleep. When the thread is notified and wakes up, it automatically re-acquires the lock before returning.
4. **Notification Scope**:
While you can call `notify_one()` or `notify_all()` while holding the mutex lock, it is often more efficient to release the lock *before* notifying if you want the waiting thread to immediately acquire the lock without blocking again.
YouTip