Master C++11 Condition Variables: From Basics to Thread‑Pool Mastery
This article explains what C++11 condition variables are, why they are essential for efficient multithreading, shows their basic usage with std::condition_variable and std::mutex, walks through a producer‑consumer example, demonstrates a thread‑pool implementation, and lists crucial best‑practice tips.
What Is a Condition Variable?
Imagine waiting in a KFC line and stepping away to the restroom; you ask a friend to call you when the line reaches you. The friend notifies you when the condition (your turn) is met. In C++ a condition variable lets one thread wait for a condition while another thread notifies it when the condition becomes true.
Why Use Condition Variables?
They prevent busy‑waiting (continuously checking a condition), allowing the waiting thread to sleep without consuming CPU cycles, similar to setting a timer instead of staring at a pot.
Basic Usage in C++11
The two main classes are std::condition_variable and its companion std::mutex. std::condition_variable – the condition variable itself. std::mutex – the mutex used to protect shared data.
Typical workflow (two steps):
1. Wait for the condition (waiting side)
std::unique_lock<std::mutex> lock(mutex); // lock first
cv.wait(lock, [&]{ return condition_met; }); // sleeps until condition is true
// lock is still held here2. Change the condition and notify (notifying side)
{
std::lock_guard<std::mutex> lock(mutex); // lock first
condition = true; // change shared state
}
cv.notify_one(); // wake one waiting thread
// or cv.notify_all(); to wake allClassic Producer‑Consumer Example
Using a bounded buffer (a tray of pancakes) to illustrate the pattern.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
using namespace std;
queue<int> products; // shared buffer
mutex mtx; // protects the buffer
condition_variable cv_empty; // buffer not full
condition_variable cv_full; // buffer not empty
const int MAX_PRODUCTS = 5;
void producer() {
for (int i = 1; i <= 10; ++i) {
unique_lock<mutex> lock(mtx);
cv_empty.wait(lock, []{ return products.size() < MAX_PRODUCTS; });
products.push(i);
cout << "Producer made " << i << " (size=" << products.size() << ")
";
lock.unlock();
cv_full.notify_one();
this_thread::sleep_for(chrono::milliseconds(300));
}
}
void consumer() {
for (int i = 1; i <= 10; ++i) {
unique_lock<mutex> lock(mtx);
cv_full.wait(lock, []{ return !products.empty(); });
int item = products.front();
products.pop();
cout << "Consumer ate " << item << " (size=" << products.size() << ")
";
lock.unlock();
cv_empty.notify_one();
this_thread::sleep_for(chrono::milliseconds(500));
}
}
int main() {
cout << "===== Breakfast shop opens =====
";
thread t1(producer);
thread t2(consumer);
t1.join();
t2.join();
cout << "===== Breakfast shop closes =====
";
return 0;
}The output shows the producer and consumer cooperating without busy‑waiting.
Key Points About Condition Variables
1. Why a while‑loop is sometimes needed
Older code used:
while (!condition_met) {
cv.wait(lock);
}This protects against spurious wake‑ups – a thread may wake without a notification, so the condition must be re‑checked.
2. Two overloads of wait()
// 1) wait(lock);
cv.wait(lock);
// 2) wait with predicate (recommended)
cv.wait(lock, []{ return condition_met; });The predicate version automatically loops until the condition is true, making the code safer.
3. Timed wait
auto status = cv.wait_for(lock, chrono::milliseconds(100), []{ return condition_met; });
if (status) {
// condition satisfied
} else {
// timeout
}Useful when you do not want to block indefinitely.
Advanced Example: Thread‑Pool Task Scheduling
A simple thread pool demonstrates how condition variables coordinate a pool of worker threads.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <functional>
using namespace std;
class ThreadPool {
vector<thread> workers;
queue<function<void()>> tasks;
mutex mtx;
condition_variable cv;
bool stop = false;
public:
ThreadPool(size_t threads) {
for (size_t i = 0; i < threads; ++i) {
workers.emplace_back([this]{
while (true) {
function<void()> task;
{
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]{ return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
template<class F> void enqueue(F&& f) {
{
unique_lock<mutex> lock(mtx);
if (stop) throw runtime_error("ThreadPool stopped");
tasks.emplace(forward<F>(f));
}
cv.notify_one();
}
~ThreadPool() {
{
unique_lock<mutex> lock(mtx);
stop = true;
}
cv.notify_all();
for (auto &worker : workers) worker.join();
}
};
int main() {
ThreadPool pool(4);
for (int i = 1; i <= 8; ++i) {
pool.enqueue([i]{
cout << "Task " << i << " starts on thread " << this_thread::get_id() << "
";
this_thread::sleep_for(chrono::seconds(1));
cout << "Task " << i << " finished
";
});
}
this_thread::sleep_for(chrono::seconds(10));
cout << "Main thread exits" << endl;
return 0;
}The pool distributes tasks among four workers, each task runs independently.
Practical Tips When Using Condition Variables
Always pair with a mutex – the condition variable alone cannot protect shared state.
Re‑check the condition after waking – spurious wake‑ups are possible, so use a while loop or the predicate overload.
Notify after changing the condition – modify the shared state first, then call notify_one or notify_all, preferably after unlocking.
Choose the right notification function – notify_one() for one‑to‑one hand‑off, notify_all() for broadcast.
Avoid lost wake‑ups – ensure the waiting thread is already blocked before sending a notification; otherwise the notification may be missed and the thread could block forever.
Summary
Condition variables are the "WeChat group notifications" of multithreaded programming: they let threads coordinate efficiently without wasting CPU time. Mastering them—understanding spurious wake‑ups, using the predicate form of wait, applying timeouts, and following the practical tips above—will elevate your C++ multithreading skills.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
