1.std::condition_variable(条件变量)
1.1.核心作用
条件变量用于线程间等待某个条件成立,配合 std::mutex 使用,解决"忙等"问题。
1.2.为什么需要它?
不用条件变量的经典错误写法:
c
// 错误:忙等待,浪费 CPU
while (!ready) { /* 空转 */ }
// 错误:sleep 轮询,响应慢且仍浪费 CPU
while (!ready) { std::this_thread::sleep_for(10ms); }
1.3.三个关键操作
c
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// 等待方
void waiter() {
std::unique_lock<std::mutex> lock(mtx);
// wait 会做两件事:
// 1. 原子地释放锁并阻塞(防止错过 notify)
// 2. 被唤醒后重新获取锁,并检查谓词
cv.wait(lock, [] { return ready; }); // 带谓词的版本(推荐)
// 等价于:
// while (!ready) cv.wait(lock);
std::cout << "条件满足,继续执行\n";
}
// 通知方
void notifier() {
{
std::lock_guard<std::mutex> lock(mtx);
ready = true; // 必须先改状态再通知
} // 先解锁再 notify,减少被唤醒线程等待锁的时间
cv.notify_one(); // 唤醒一个等待线程
// cv.notify_all(); // 唤醒所有等待线程
}
为什么必须配合 unique_lock?
wait需要在阻塞时释放锁,被唤醒时重新加锁------锁的所有权需要可转移,unique_lock满足(lock_guard不行)。- 这也是
wait接收unique_lock&而非mutex&的原因。
经典坑点:
- 虚假唤醒(
spurious wakeup):wait可能在没有notify的情况下返回,所以必须用while循环或带谓词版本检查条件。 - 先改状态再通知:否则可能通知后、等待方还没进入
wait,导致永久阻塞(进入wait后,不再发通知了)。 wait返回时不保证条件仍成立:被唤醒到重新拿到锁之间,其他线程可能又改了状态。不过拿到锁后,会进行谓词检测,可以保证此时谓词检测不通过,释放锁,再次等待。
典型应用:生产者-消费者队列
c
template <typename T>
class BlockingQueue {
std::mutex mtx;
std::condition_variable cv_not_empty, cv_not_full;
std::queue<T> q;
size_t capacity;
public:
void push(T val) {
std::unique_lock<std::mutex> lock(mtx);
cv_not_full.wait(lock, [&] { return q.size() < capacity; });
q.push(std::move(val));
cv_not_empty.notify_one();
}
T pop() {
std::unique_lock<std::mutex> lock(mtx);
cv_not_empty.wait(lock, [&] { return !q.empty(); });
T val = std::move(q.front());
q.pop();
cv_not_full.notify_one();
return val;
}
};
1.3.1.条件等待细节
带谓词的 cv.wait(lock, pred) 的标准语义等价于:
c
// 标准库大致实现
template <typename Lock, typename Pred>
void wait(Lock& lock, Pred pred) {
while (!pred()) { // 注意:pred 的调用发生在持有锁期间
wait(lock); // 释放锁 + 阻塞(原子操作),被唤醒后重新加锁
}
}
完整流程拆解:
- 进入时(还没睡眠过):
c
加锁 → 检查 pred()
├─ true → 直接通过,不睡眠
└─ false → 释放锁(原子地进入阻塞)
- 被唤醒后:
c
从阻塞返回 → 重新获得锁 → 检查 pred()
├─ true → 返回,继续执行业务逻辑
└─ false → 再次释放锁,重新进入阻塞
2.std::condition_variable_any
与 condition_variable 的区别
std::condition_variable 只能与 std::mutex 配合使用,而 condition_variable_any 可以与任何满足基本锁要求的互斥类型一起工作(BasicLockable:只需有 lock() 和 unlock())。
c
std::condition_variable_any cv_any;
std::shared_mutex smtx; // shared_mutex 不是普通 mutex!
void reader() {
std::shared_lock lock(smtx); // 读锁
cv_any.wait(lock, [] { return data_ready; });
}
void writer() {
{
std::unique_lock lock(smtx);
data_ready = true;
}
cv_any.notify_all();
}
代价:
condition_variable_any 是通用实现,性能略低于专门优化的 std::condition_variable。除非确实需要非标准互斥类型(如 shared_mutex、用户自定义锁、甚至跨进程的锁),否则优先用 condition_variable。
3.std::promise(承诺)
3.1.核心模型
promise 是一次性单写者通道:某线程通过 set_value / set_exception 写入结果,结果自动存到关联的 shared state(共享状态)中,供未来的 future 读取。
c
std::promise<int> p;
std::future<int> f = p.get_future(); // 必须在 set_value 之前获取
std::thread t([&p] {
std::this_thread::sleep_for(1s);
p.set_value(42); // 写入结果,此后 future 可读到
});
std::cout << f.get(); // 阻塞直到结果就绪
t.join();
set_value 之后:
set_value 只能调用一次(第二次调用抛 std::future_error)。它是线程安全的------可与 future::get 并发执行。
异常传递:
c
std::thread t([&p] {
try {
throw std::runtime_error("出错了");
} catch (...) {
p.set_exception(std::current_exception()); // 把异常存进共享状态
}
});
try {
f.get(); // 会重新抛出该异常
} catch (const std::exception& e) {
std::cout << e.what();
}
使用场景:
当你自己创建线程并希望它向调用方返回结果/异常时,promise 是最底层的原语(async 和 packaged_task 都是基于它构建的)。
4.std::future(期物)
核心模型:
future 是共享状态的读端句柄,代表一个"未来才会有的值"。三种就绪方式:
promise::set_value赋值packaged_task执行完毕async任务完成
关键成员函数:
c
std::future<int> f = ...;
int v = f.get(); // 阻塞直到就绪;只能调用一次;移动语义取出结果
f.wait(); // 仅阻塞等待,不取结果
f.wait_for(100ms); // 限时等待,返回 future_status
f.wait_until(tp);
f.valid(); // 是否关联了共享状态
f.share(); // 转为 shared_future(调用后本对象失效)
future_status:
c
auto status = f.wait_for(500ms);
switch (status) {
case std::future_status::ready: /* 就绪 */ break;
case std::future_status::timeout: /* 超时 */ break;
case std::future_status::deferred: /* 任务延迟未启动(launch::deferred)*/ break;
}
关键限制:
future 是一次性、独占的:get() 只能调用一次,第二次会抛 future_error(因为结果是被 move 出来的)。这也是 shared_future 存在的理由。
析构行为(重要陷阱):
- 如果
future关联的是std::async启动的任务且未get/wait,future析构时会阻塞直到任务完成。 - 如果关联的是
deferred任务,析构不会执行该任务(任务被丢弃)。
5.std::shared_future
与 future 的区别:
shared_future 允许多个线程多次读取同一个结果。
c
std::promise<int> p;
std::shared_future<int> sf = p.get_future().share(); // 或 p.get_future() 隐式转换
// 多个线程可同时 get()
std::thread t1([sf] { std::cout << sf.get(); });
std::thread t2([sf] { std::cout << sf.get(); }); // OK,可复制
| 特性 | future |
shared_future |
|---|---|---|
| 复制 | 不可复制,只可移动 | 可复制 |
get() 次数 |
仅一次 | 任意多次,并发安全 |
| 典型用途 | 一对一传递结果 | 广播结果给多个等待者 |
使用场景:
一次计算、多处消费,例如:
c
// 主线程加载配置,多个工作线程等待配置就绪
std::shared_future<Config> config_ready = std::async(std::launch::async, load_config).share();
std::thread workers[4];
for (auto& w : workers)
w = std::thread([config_ready] { process(config_ready.get()); });
注意:shared_future 本身对象不是线程安全的(复制/析构需外部同步),但并发调用 get() 是安全的。
6.std::packaged_task
核心模型
把可调用对象 + 结果通道打包在一起:包装后的函数对象被调用时,返回值(或异常)自动存入关联的 shared state。
c
std::packaged_task<int(int, int)> task([](int a, int b) { return a + b; });
std::future<int> f = task.get_future();
task(3, 4); // 在任意线程调用(不一定在创建它的线程)
std::cout << f.get(); // 7
关键用途:线程池
packaged_task 是构建任务队列/线程池的标准方式------调用方不关心任务在哪个线程执行,只通过 future 取结果:
c
class ThreadPool {
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx; std::condition_variable cv;
bool stop = false;
public:
ThreadPool(size_t n) {
for (size_t i = 0; i < n; ++i)
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{ std::unique_lock lk(mtx);
cv.wait(lk, [&] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front()); tasks.pop(); }
task();
}
});
}
template <typename F, typename... Args>
auto submit(F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>> {
using R = std::invoke_result_t<F, Args...>;
auto task = std::make_shared<std::packaged_task<R()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...));
std::future<R> fut = task->get_future();
{ std::lock_guard lk(mtx); tasks.emplace([task] { (*task)(); }); }
cv.notify_one();
return fut;
}
~ThreadPool() {
{ std::lock_guard lk(mtx); stop = true; }
cv.notify_all();
for (auto& w : workers) w.join();
}
};
// 使用
ThreadPool pool(4);
auto fut = pool.submit([](int x) { return x * x; }, 5);
std::cout << fut.get(); // 25
6.1.逐行拆解 submit 的实现
6.1.1.模板签名部分
- 万能引用(
forwarding reference)
c
template <typename F, typename... Args>
auto submit(F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>>
F&& f 中 F 是推导出来的模板参数,所以 F&& 是万能引用而非右值引用:
| 调用方式 | F 推导为 |
f 的类型 |
|---|---|---|
submit([](int x){...}, 5) |
lambda 类型(值) | lambda 的左值引用 |
submit(std::move(func_obj), 5) |
同类型 | 右值引用 |
submit(some_func, 5) |
函数指针类型 | 指针的左值引用 |
配合后面的 std::forward 实现完美转发:左值保持左值、右值保持右值,lambda 既可以拷贝也可以移动进来。
std::invoke_result_t<F, Args...>------ 推导返回值类型
c
auto submit(...) -> std::future<std::invoke_result_t<F, Args...>>
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// "用 F 调用 Args... 会返回什么类型?"
C++17 引入,等价于 typename std::invoke_result<F, Args...>::type。
6.1.2.打包任务
c
using R = std::invoke_result_t<F, Args...>;
auto task = std::make_shared<std::packaged_task<R()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...));
std::packaged_task<R()>------ 零参可调用的打包器
packaged_task的模板参数是函数签名。R()表示"无参数、返回R"。- std::bind ------ 参数绑定 + 值类别转发
c
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
bind 会拷贝或移动所有实参到内部存储,生成一个新的可调用对象。
6.1.3.为什么用 std::make_shared 而不是直接放 packaged_task?
c
std::queue<std::function<void()>> tasks;
std::function<void()> 要求可调用对象可拷贝。而:
std::packaged_task是只可移动、不可拷贝的std::bind绑定出lambda也可能因捕获移动-only类型而不可拷贝。
解决方案:用 shared_ptr 做一层间接------shared_ptr 本身可拷贝,拷贝的只是控制块引用计数:
c
auto task = std::make_shared<std::packaged_task<R()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...));
// 队列里存的 lambda 捕获 shared_ptr(可拷贝)
tasks.emplace([task] { (*task)(); });
生命周期问题也随之解决:submit 返回后局部 task 销毁,但队列中的 lambda 还持有一份引用,任务对象活到执行完毕。若没有这层 shared_ptr,任务对象在 submit 结束时就销毁了。
6.1.4.获取 future
c
std::future<R> fut = task->get_future();
future 只可移动,return fut; 触发移动构造/移动返回值优化,把读取端交还给调用者。
6.1.5.入队与通知
c
{ std::lock_guard lk(mtx); tasks.emplace([task] { (*task)(); }); }
cv.notify_one();
- 先加锁,放入,释放锁,再通知。可以包装通知唤醒等待者时,等待者可以立即获得锁。
- 唤醒一个,是因为一次只放入一个任务。唤醒所有,会导致其余唤醒者无意义唤醒。
tasks.emplace(...) vs tasks.push(...)
直接构造lambda进队列,相比lambda的临时对象构造+移动,是顺手的小优化。
7.std::async(异步运行)
三种启动策略
c
auto f1 = std::async(func); // 默认:实现决定(可能 async 或 deferred)
auto f2 = std::async(std::launch::async, func); // 强制新线程立即执行
auto f3 = std::async(std::launch::deferred, func); // 延迟:调用 get()/wait() 时才在当前线程执行
auto f4 = std::async(std::launch::async | std::launch::deferred, func); // 任选其一
各策略语义
| 策略 | 执行时机 | 线程 | 特点 |
|---|---|---|---|
async |
调用即启动 | 新线程 | 真正并行 |
deferred |
首次 get()/wait() |
调用 get 的线程 | 惰性求值,可能永远不执行 |
| 默认 | 未指定 | 未指定 | 不可移植,不要依赖 |
注意 deferred 的陷阱
c
auto f = std::async(std::launch::deferred, [] { std::cout << "任务\n"; });
// 如果不调用 f.get(),任务永远不会执行
f.get(); // 此时才在当前线程执行
future 析构阻塞陷阱:
c
void bad() {
auto f = std::async(std::launch::async, [] {
std::this_thread::sleep_for(10s);
});
// f 离开作用域析构时会阻塞 ~10 秒!
} // 阻塞点
异常传递:
c
auto f = std::async([] { throw std::runtime_error("失败"); });
try { f.get(); }
catch (const std::exception& e) { /* 捕获到任务中的异常 */ }
async vs thread 的选择:
std::thread |
std::async |
|
|---|---|---|
| 返回值 | 无(需自行用 promise) | 有 future |
| 异常处理 | 线程内未捕获异常 → terminate | 异常经 future 传递 |
| 线程创建 | 手动管理 | 实现可复用线程(如线程池) |
| 适用 | 长生命周期、手动精细控制 | "想要个结果"的临时任务 |
8.整体关系图
c
┌─────────────────────────────────────────┐
│ 共享状态 (shared state) │
│ (由实现管理的引用计数对象) │
└──────▲──────────────▲──────────────▲─────┘
│ │ │
写入端(三选一) │ │ │ 读取端
┌───────────────────────┼──────────────┼──────────────┼──────────────┐
│ │ │ │ │
std::promise std::packaged_task std::async std::future std::shared_future
set_value() 包装可调用对象, 启动策略 + 独占读取 共享读取
set_exception() 调用时自动写结果 自动绑定 get()一次 get()多次
│ │ │ │
└───────────────────────┴──────────────┴──────────────┘
底层机制:promise 是地基;packaged_task 和 async 内部都基于它
9.如何选择
| 需求 | 推荐组件 |
|---|---|
| 线程间等待条件 | condition_variable + mutex |
| 需要与 shared_mutex 等非常规锁配合等待 | condition_variable_any |
| 提交任务到线程池并取结果 | packaged_task + 手写队列 |
| 快速启动异步任务取结果 | std::async |
| 手动创建线程传递结果/异常 | std::promise |
| 一个结果多个消费者 | std::shared_future |
| 一对一传递结果 | std::future |
经验法则:
- 能用高层抽象就不用底层------
async优先于手写thread + promise; packaged_task优先于在线程函数里手动set_value;- 带谓词的
cv.wait(lock, pred)优先于裸while循环。