1.std::condition_variable(条件变量)
1.1.核心作用
条件变量用于线程间等待某个条件成立,配合 std::mutex 使用,解决"忙等"问题。
1.2.为什么需要它?
不用条件变量的经典错误写法:
// 错误:忙等待,浪费 CPUwhile(!ready){/* 空转 */}// 错误:sleep 轮询,响应慢且仍浪费 CPUwhile(!ready){std::this_thread::sleep_for(10ms);}1.3.三个关键操作
std::mutex mtx;std::condition_variable cv;bool ready=false;// 等待方voidwaiter(){std::unique_lock<std::mutex>lock(mtx);// wait 会做两件事:// 1. 原子地释放锁并阻塞(防止错过 notify)// 2. 被唤醒后重新获取锁,并检查谓词cv.wait(lock,[]{returnready;});// 带谓词的版本(推荐)// 等价于:// while (!ready) cv.wait(lock);std::cout<<"条件满足,继续执行\n";}// 通知方voidnotifier(){{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返回时不保证条件仍成立:被唤醒到重新拿到锁之间,其他线程可能又改了状态。不过拿到锁后,会进行谓词检测,可以保证此时谓词检测不通过,释放锁,再次等待。
典型应用:生产者-消费者队列
template<typename T>class BlockingQueue{std::mutex mtx;std::condition_variable cv_not_empty,cv_not_full;std::queue<T>q;size_tcapacity;public:voidpush(T val){std::unique_lock<std::mutex>lock(mtx);cv_not_full.wait(lock,[&]{returnq.size()<capacity;});q.push(std::move(val));cv_not_empty.notify_one();}Tpop(){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();returnval;}};1.3.1.条件等待细节
带谓词的cv.wait(lock, pred)的标准语义等价于:
// 标准库大致实现template<typename Lock,typename Pred>voidwait(Lock&lock,Pred pred){while(!pred()){// 注意:pred 的调用发生在持有锁期间wait(lock);// 释放锁 + 阻塞(原子操作),被唤醒后重新加锁}}完整流程拆解:
- 进入时(还没睡眠过):
加锁 → 检查pred()├─ true → 直接通过,不睡眠 └─ false → 释放锁(原子地进入阻塞)- 被唤醒后:
从阻塞返回 → 重新获得锁 → 检查pred()├─ true → 返回,继续执行业务逻辑 └─ false → 再次释放锁,重新进入阻塞2.std::condition_variable_any
与condition_variable的区别std::condition_variable只能与std::mutex配合使用,而condition_variable_any可以与任何满足基本锁要求的互斥类型一起工作(BasicLockable:只需有lock()和unlock())。
std::condition_variable_any cv_any;std::shared_mutex smtx;// shared_mutex 不是普通 mutex!voidreader(){std::shared_locklock(smtx);// 读锁cv_any.wait(lock,[]{returndata_ready;});}voidwriter(){{std::unique_locklock(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读取。
std::promise<int>p;std::future<int>f=p.get_future();// 必须在 set_value 之前获取std::threadt([&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并发执行。
异常传递:
std::threadt([&p]{try{throw std::runtime_error("出错了");}catch(...){p.set_exception(std::current_exception());// 把异常存进共享状态}});try{f.get();// 会重新抛出该异常}catch(conststd::exception&e){std::cout<<e.what();}使用场景:
当你自己创建线程并希望它向调用方返回结果/异常时,promise是最底层的原语(async和packaged_task都是基于它构建的)。
4.std::future(期物)
核心模型:future是共享状态的读端句柄,代表一个"未来才会有的值"。三种就绪方式:
promise::set_value赋值packaged_task执行完毕async任务完成
关键成员函数:
std::future<int>f=...;intv=f.get();// 阻塞直到就绪;只能调用一次;移动语义取出结果f.wait();// 仅阻塞等待,不取结果f.wait_for(100ms);// 限时等待,返回 future_statusf.wait_until(tp);f.valid();// 是否关联了共享状态f.share();// 转为 shared_future(调用后本对象失效)future_status:
autostatus=f.wait_for(500ms);switch(status){casestd::future_status::ready:/* 就绪 */break;casestd::future_status::timeout:/* 超时 */break;casestd::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允许多个线程多次读取同一个结果。
std::promise<int>p;std::shared_future<int>sf=p.get_future().share();// 或 p.get_future() 隐式转换// 多个线程可同时 get()std::threadt1([sf]{std::cout<<sf.get();});std::threadt2([sf]{std::cout<<sf.get();});// OK,可复制| 特性 | future | shared_future |
|---|---|---|
| 复制 | 不可复制,只可移动 | 可复制 |
get()次数 | 仅一次 | 任意多次,并发安全 |
| 典型用途 | 一对一传递结果 | 广播结果给多个等待者 |
使用场景:
一次计算、多处消费,例如:
// 主线程加载配置,多个工作线程等待配置就绪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。
std::packaged_task<int(int,int)>task([](inta,intb){returna+b;});std::future<int>f=task.get_future();task(3,4);// 在任意线程调用(不一定在创建它的线程)std::cout<<f.get();// 7关键用途:线程池packaged_task是构建任务队列/线程池的标准方式——调用方不关心任务在哪个线程执行,只通过future取结果:
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_tn){for(size_ti=0;i<n;++i)workers.emplace_back([this]{while(true){std::function<void()>task;{std::unique_locklk(mtx);cv.wait(lk,[&]{returnstop||!tasks.empty();});if(stop&&tasks.empty())return;task=std::move(tasks.front());tasks.pop();}task();}});}template<typename F,typename...Args>autosubmit(F&&f,Args&&...args)->std::future<std::invoke_result_t<F,Args...>>{using R=std::invoke_result_t<F,Args...>;autotask=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_guardlk(mtx);tasks.emplace([task]{(*task)();});}cv.notify_one();returnfut;}~ThreadPool(){{std::lock_guardlk(mtx);stop=true;}cv.notify_all();for(auto&w:workers)w.join();}};// 使用ThreadPoolpool(4);autofut=pool.submit([](intx){returnx*x;},5);std::cout<<fut.get();// 256.1.逐行拆解 submit 的实现
6.1.1.模板签名部分
- 万能引用(
forwarding reference)
template<typename F,typename...Args>autosubmit(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...>—— 推导返回值类型
autosubmit(...)->std::future<std::invoke_result_t<F,Args...>>// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^// "用 F 调用 Args... 会返回什么类型?"C++17引入,等价于typename std::invoke_result<F, Args...>::type。
6.1.2.打包任务
using R=std::invoke_result_t<F,Args...>;autotask=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 —— 参数绑定 + 值类别转发
std::bind(std::forward<F>(f),std::forward<Args>(args)...)bind会拷贝或移动所有实参到内部存储,生成一个新的可调用对象。
6.1.3.为什么用 std::make_shared 而不是直接放 packaged_task?
std::queue<std::function<void()>>tasks;std::function<void()>要求可调用对象可拷贝。而:
std::packaged_task是只可移动、不可拷贝的std::bind绑定出lambda也可能因捕获移动-only类型而不可拷贝。
解决方案:用shared_ptr做一层间接——shared_ptr本身可拷贝,拷贝的只是控制块引用计数:
autotask=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
std::future<R>fut=task->get_future();future只可移动,return fut;触发移动构造/移动返回值优化,把读取端交还给调用者。
6.1.5.入队与通知
{std::lock_guardlk(mtx);tasks.emplace([task]{(*task)();});}cv.notify_one();- 先加锁,放入,释放锁,再通知。可以包装通知唤醒等待者时,等待者可以立即获得锁。
- 唤醒一个,是因为一次只放入一个任务。唤醒所有,会导致其余唤醒者无意义唤醒。
tasks.emplace(...) vs tasks.push(...)
直接构造lambda进队列,相比lambda的临时对象构造+移动,是顺手的小优化。
7.std::async(异步运行)
三种启动策略
autof1=std::async(func);// 默认:实现决定(可能 async 或 deferred)autof2=std::async(std::launch::async,func);// 强制新线程立即执行autof3=std::async(std::launch::deferred,func);// 延迟:调用 get()/wait() 时才在当前线程执行autof4=std::async(std::launch::async|std::launch::deferred,func);// 任选其一各策略语义
| 策略 | 执行时机 | 线程 | 特点 |
|---|---|---|---|
async | 调用即启动 | 新线程 | 真正并行 |
deferred | 首次get()/wait() | 调用 get 的线程 | 惰性求值,可能永远不执行 |
| 默认 | 未指定 | 未指定 | 不可移植,不要依赖 |
注意deferred的陷阱
autof=std::async(std::launch::deferred,[]{std::cout<<"任务\n";});// 如果不调用 f.get(),任务永远不会执行f.get();// 此时才在当前线程执行future析构阻塞陷阱:
voidbad(){autof=std::async(std::launch::async,[]{std::this_thread::sleep_for(10s);});// f 离开作用域析构时会阻塞 ~10 秒!}// 阻塞点异常传递:
autof=std::async([]{throw std::runtime_error("失败");});try{f.get();}catch(conststd::exception&e){/* 捕获到任务中的异常 */}async vs thread的选择:
std::thread | std::async | |
|---|---|---|
| 返回值 | 无(需自行用 promise) | 有 future |
| 异常处理 | 线程内未捕获异常 → terminate | 异常经 future 传递 |
| 线程创建 | 手动管理 | 实现可复用线程(如线程池) |
| 适用 | 长生命周期、手动精细控制 | "想要个结果"的临时任务 |
8.整体关系图
┌─────────────────────────────────────────┐ │ 共享状态(shared state)│ │ (由实现管理的引用计数对象) │ └──────▲──────────────▲──────────────▲─────┘ │ │ │ 写入端(三选一) │ │ │ 读取端 ┌───────────────────────┼──────────────┼──────────────┼──────────────┐ │ │ │ │ │ std::promise std::packaged_task std::async std::future std::shared_futureset_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循环。