news 2026/9/13 23:09:13

c++并发--同步

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
c++并发--同步

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是最底层的原语(asyncpackaged_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/waitfuture析构时会阻塞直到任务完成。
  • 如果关联的是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,可复制
特性futureshared_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();// 25

6.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&& fF是推导出来的模板参数,所以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::threadstd::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循环。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 23:05:52

【2026年】定风量阀与变风量阀价差多少?采购决策分析

做通风系统预算时&#xff0c;"定风量阀还是变风量阀"是最先要拍板的选型&#xff0c;而价差往往是决策关键。定风量阀便宜、结构简单&#xff0c;变风量阀贵、但能按需调节省钱。这篇把两者的价差逻辑、省账算清&#xff0c;帮你在安全与成本之间做出理性选择。一、…

作者头像 李华
网站建设 2026/9/13 22:51:20

【HarmonyOS 7新能力|007】空间音频入门实战:从能力边界到最小可运行链路

【HarmonyOS 7新能力&#xff5c;007】空间音频入门实战&#xff1a;从能力边界到最小可运行链路 空间音频并不是给左右声道各调一次音量。应用要先描述听者和声源的相对位置&#xff0c;再把位置、方向、播放状态与页面生命周期持续同步给渲染能力。只要坐标轴约定不一致、节点…

作者头像 李华