Advanced Thread Synchronization
Beyond mutexes and atomics, C++ provides higher-level synchronization primitives: condition variables for signaling between threads, and futures/promises for asynchronous result retrieval.
Condition Variables
Condition variables enable threads to wait for specific conditions without busy-waiting:
#include <condition_variable>
#include <mutex>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> queue;
void producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
queue.push(i);
}
cv.notify_one(); // Wake one waiting thread
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !queue.empty(); }); // Wait until queue not empty
int value = queue.front();
queue.pop();
lock.unlock();
std::cout << "Consumed: " << value << '\\n';
}
}
Wait Mechanics
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock); // Atomically unlocks mtx and sleeps
// When woken, reacquires mtx
// Equivalent to:
while (!predicate()) {
cv.wait(lock);
}
Spurious Wakeups
Condition variables can wake spuriously (without notify). Always use predicate form:
// Bad: No protection against spurious wakeups
cv.wait(lock);
if (!queue.empty()) { /* ... */ }
// Good: Handles spurious wakeups
cv.wait(lock, []{ return !queue.empty(); });
notify_one vs notify_all
cv.notify_one(); // Wakes one waiting thread
cv.notify_all(); // Wakes all waiting threads
Use notify_all when multiple threads might need to act on the condition.
Timed Waits
using namespace std::chrono_literals;
std::unique_lock<std::mutex> lock(mtx);
if (cv.wait_for(lock, 1s, []{ return ready; })) {
// Condition met within 1 second
} else {
// Timeout
}
auto deadline = std::chrono::system_clock::now() + 5s;
if (cv.wait_until(lock, deadline, []{ return ready; })) {
// Condition met before deadline
}
std::future and std::promise
Futures provide a mechanism to retrieve results from asynchronous operations:
promise-future Pair
#include <future>
void compute(std::promise<int> prom) {
// Do expensive computation
int result = 42;
prom.set_value(result); // Fulfill promise
}
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(compute, std::move(prom));
int result = fut.get(); // Blocks until promise fulfilled
std::cout << "Result: " << result << '\\n';
t.join();
Exception Propagation
void task(std::promise<int> prom) {
try {
throw std::runtime_error("Error!");
} catch (...) {
prom.set_exception(std::current_exception());
}
}
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(task, std::move(prom));
try {
int result = fut.get(); // Rethrows exception
} catch (const std::exception& e) {
std::cout << "Caught: " << e.what() << '\\n';
}
t.join();
std::async
Higher-level alternative to manual thread + promise:
std::future<int> fut = std::async(std::launch::async, []{
// Runs in separate thread
return 42;
});
int result = fut.get(); // Retrieve result
Launch Policies
// Force async execution (new thread)
auto f1 = std::async(std::launch::async, task);
// Defer execution until get() called (lazy)
auto f2 = std::async(std::launch::deferred, task);
// Implementation chooses (default)
auto f3 = std::async(task);
shared_future
Multiple threads can wait on the same result:
std::shared_future<int> sf = std::async(task).share();
auto lambda = [sf]{ return sf.get(); };
std::thread t1(lambda);
std::thread t2(lambda);
// Both can call get()
t1.join();
t2.join();
packaged_task
Wraps callable for future retrieval:
std::packaged_task<int(int, int)> task([](int a, int b) {
return a + b;
});
std::future<int> result = task.get_future();
std::thread t(std::move(task), 3, 4);
std::cout << result.get(); // 7
t.join();
Producer-Consumer with Condition Variables
class ThreadSafeQueue {
std::queue<int> queue_;
mutable std::mutex mtx_;
std::condition_variable cv_;
public:
void push(int value) {
{
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(value);
}
cv_.notify_one();
}
int pop() {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this]{ return !queue_.empty(); });
int value = queue_.front();
queue_.pop();
return value;
}
bool empty() const {
std::lock_guard<std::mutex> lock(mtx_);
return queue_.empty();
}
};
Conceptual Check
Why must condition_variable::wait take a unique_lock instead of lock_guard?
Runtime Environment
Interactive Lab
1#include <iostream>
2#include <future>
3#include <thread>
4#include <chrono>
5
6int compute(int x) {
7 std::this_thread::sleep_for(std::chrono::milliseconds(100));
8 return x * x;
9}
10
11int main() {
12 // Async execution
13 auto fut1 = std::async(std::launch::async, compute, 5);
14 auto fut2 = std::async(std::launch::async, compute, 10);
15
16 std::cout << "Computing...\n";
17
18 std::cout << "Result 1: " << fut1.get() << '\n';
19 std::cout << "Result 2: " << fut2.get() << '\n';
20
21 return 0;
22}
System Console
Waiting for signal...