Condition Variables in C++
Introduction
Condition variables are a synchronization primitive that enable threads to wait until a particular condition occurs. They are used in conjunction with a mutex to allow threads to efficiently wait for events or conditions to be met without busy-waiting.
Why Use Condition Variables?
In multithreaded programming, it's common for threads to wait for each other to reach a certain state or for certain events to occur. Condition variables allow threads to sleep until they are woken up by another thread, thus avoiding wasteful CPU usage.
Basic Usage
Condition variables in C++ are provided by the <condition_variable> header. They need to be used alongside a mutex to ensure proper synchronization. Below is a simple example demonstrating the use of condition variables.
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> std::mutex mtx; std::condition_variable cv; bool ready = false; void print_id(int id) { std::unique_lock<std::mutex> lck(mtx); while (!ready) cv.wait(lck); // After the wait, we own the lock. std::cout << "Thread " << id << '\n'; } void go() { std::unique_lock<std::mutex> lck(mtx); ready = true; cv.notify_all(); } int main() { std::thread threads[10]; // Spawn 10 threads: for (int i=0; i<10; ++i) threads[i] = std::thread(print_id,i); std::cout << "10 threads ready to race...\n"; go(); // Go! for (auto& th : threads) th.join(); return 0; }
Output:
10 threads ready to race... Thread 0 Thread 1 Thread 2 Thread 3 Thread 4 Thread 5 Thread 6 Thread 7 Thread 8 Thread 9
Explanation
In the example above, we create a global condition variable cv and a global mutex mtx. We also have a boolean flag ready to indicate when the condition is met.
The print_id function waits for the ready flag to become true by calling cv.wait(lck), where lck is a unique lock on the mutex. The go function sets ready to true and notifies all waiting threads by calling cv.notify_all().
Spurious Wakeups
Condition variables can sometimes experience spurious wakeups, where a waiting thread is woken up without any notification. To handle this, the condition should always be checked in a loop:
std::unique_lock<std::mutex> lck(mtx); while (!ready) cv.wait(lck);
Real-world Example
Let's consider a real-world example where a producer thread generates data and a consumer thread processes it. The consumer thread waits for the producer to notify it when new data is available.
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <queue> std::mutex mtx; std::condition_variable cv; std::queue<int> dataQueue; bool done = false; void producer() { for (int i = 0; i < 10; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::unique_lock<std::mutex> lck(mtx); dataQueue.push(i); cv.notify_one(); } std::unique_lock<std::mutex> lck(mtx); done = true; cv.notify_one(); } void consumer() { while (true) { std::unique_lock<std::mutex> lck(mtx); cv.wait(lck, []{ return !dataQueue.empty() || done; }); if (!dataQueue.empty()) { int data = dataQueue.front(); dataQueue.pop(); lck.unlock(); std::cout << "Processed data: " << data << '\n'; } else if (done) { break; } } } int main() { std::thread prod(producer); std::thread cons(consumer); prod.join(); cons.join(); return 0; }
Output:
Processed data: 0 Processed data: 1 Processed data: 2 Processed data: 3 Processed data: 4 Processed data: 5 Processed data: 6 Processed data: 7 Processed data: 8 Processed data: 9
Conclusion
Condition variables are a powerful tool for synchronizing threads and managing communication between them. They allow threads to wait for specific conditions and be notified when those conditions are met. By combining condition variables with mutexes, you can create efficient and responsive multithreaded applications.