YouTip LogoYouTip

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.
← Cpp Libs AtomicCpp Libs Thread β†’