
写多线程程序时最尴尬的场面任务在线程里跑完了主线程却不知道去哪儿拿结果——直接读共享变量怕数据竞争sleep 一段时间去“赌”任务完成又不靠谱。常规做法要么用全局变量加互斥锁硬传值要么靠延时等待前者极易埋下竞态后者既浪费时间又无法保证正确任务一旦抛异常结果更是直接丢失。本文分享一套标准库自带的结果回传方案promise 写入 future 等待 async 启动再补上 packaged_task 封装与 shared_future 分发可直接落地。一、异步结果回传先看懂这套通信骨架异步结果要能“回得来”先得认清这五个角色各自站在通道的哪一端future读端持有共享状态的只读引用靠get()/wait()取结果。promise写端一次性写入值或异常写完即可与读端解耦。shared_future允许get()被多次调用多方同时等待同一结果。packaged_task把可调用对象包起来执行后自动写入关联 future。std::async启动策略 自动打包最省事的入口函数。核心结论五者共享同一套“共享状态”抽象差别只在谁写、谁读、能读几次。二、promise 与 future通道两端各司其职想先跑通最基础的一次性传递从 promise 和 future 这对搭档开始#includefuture#includethread#includeiostreamintmain(){std::promiseintprom;std::futureintfutprom.get_future();std::threadt([prom]{prom.set_value(42);});std::coutfut.get()std::endl;// 值没到就阻塞t.join();}强约束写入 → 通道缓冲 → 读端 get() 唤醒返回get() 会阻塞值未到就挂起当前线程值到达后被唤醒并返回。写入只此一次重复调用set_value会抛std::future_error。异常也能传set_exception写入后读端get()会原样重抛。核心结论promise 负责投递future 负责等待两者靠共享状态松耦合。三、std::async最省事的任务启动方式不想手动管理线程和通道std::async一行启动并顺手返回 future#includefuture#includeiostreamintheavy_calc(intn){/* 耗时计算 */returnn*n;}intmain(){autof1std::async(std::launch::async,heavy_calc,10);autof2std::async(std::launch::deferred,heavy_calc,20);std::coutf1.get()f2.get()std::endl;// f2 此刻才执行}启动 打包 回传一步到位是日常代码的默认选择返回值自动入通道lambda 的返回值会被打包写入关联 future异常同理。析构可能等任务async返回的 future 在析构时会阻塞等待任务结束。别依赖默认策略不传策略时由实现自定生产代码要显式写明。核心结论std::async 启动 打包 回传一行拿到可用的 future。四、launch 策略决定任务在哪条线程跑选错启动策略轻则浪费线程重则让“异步”悄悄变回同步执行策略执行位置阻塞时机适用场景std::launch::async新线程立即执行真并发、需立即返回std::launch::deferred调用 get/wait 的线程get/wait 时才执行条件满足再算的延迟任务默认二者皆可实现自定实现自定示例代码、非关键路径deferred 并非异步任务在调用者线程执行嵌套调用时有死锁风险。显式写策略关键路径永远显式传async或deferred不猜实现。核心结论策略决定“谁来执行”这是 async 唯一需要你亲自拍板的参数。五、packaged_task把可调用对象封装成任务任务不是现成 lambda、或要把执行权交给别的线程时用 packaged_task 打包#includefuture#includethreadintmain(){std::packaged_taskint()pt([]{return6*7;});std::futureintfpt.get_future();std::threadworker([](std::packaged_taskint()t){t();},std::move(pt));// 移交执行权intvf.get();// 结果自动回到主线程worker.join();returnv42?0:1;}执行与回传绑定在同一个对象上天然适配任务队列只可移动不可复制执行权唯一移交后原位置不能再调用。不执行就没结果任务从未被调用读端get()会抛broken_promise。适合队列分发std::queuepackaged_task... worker 线程是线程池雏形。核心结论packaged_task 是手写线程池的基础件把“跑”和“回”绑在一起。六、shared_future一份结果多方取用一个结果要通知多个等待者普通 future 取一次就失效了#includefuture#includethread#includeiostreamautosfstd::async(std::launch::async,[]{returnload_config();}).share();std::threada([sf]{std::coutAsf.get()std::endl;});std::threadb([sf]{std::coutBsf.get()std::endl;});a.join();b.join();// 两个线程读到同一份结果写一次、读多次这正是 shared_future 存在的全部理由share() 完成转换future::share()把独占读端转成共享读端原 future 失效。get() 可重复调用返回const T多个线程可同时等待同一结果。写端仍然唯一promise 只能 set 一次被共享的只是读取侧。核心结论广播场景用 shared_future避免为每个消费者各复制一份数据。七、异常与返回值结果回传的两条通路结果回传不只有返回值异常也必须顺着同一条通道回到调用方情况写端动作读端表现正常返回set_value或任务返回get()返回该值任务抛异常自动set_exceptionget()原样重抛写端提前析构未 set 就销毁 promiseget()抛broken_promise任务从未执行packaged_task 未调用get()抛future_error异常不会丢async与packaged_task会自动捕获并存入共享状态。get() 是唯一出口返回值和异常都从这一个函数出来调用方必须 try 包住。核心结论把get()放进 try/catch是异步代码最不能省的两行。八、wait_for 超时别让主线程无限期挂起别让get()无条件阻塞先用wait_for给等待设一个上限#includefuture#includechronoautofutstd::async(std::launch::async,[]{returnslow_io();});if(fut.wait_for(std::chrono::seconds(2))std::future_status::ready){use(fut.get());// 正常拿到结果}else{fallback();// 走降级future 仍有效可再等}先问一句“好了没”再决定拿值还是降级三种状态ready有结果、timeout超时、deferred任务尚未执行。超时不丢通道超时后 future 依然有效可以再次等待或直接get()。deferred 特殊对 deferred 任务等待不会真的耗时直接返回deferred。核心结论等待带超时异步才不会把主线程一起拖死。九、五类高频翻车点定位与修复清单这些崩溃和卡死九成来自对通道生命周期的误解现象根因修复方式get()抛 broken_promise写端未 set 就析构每条路径都 set 或 set_exception程序卡住不退出async 的 future 析构时等待任务显式 wait/get 或改用显式线程重复取值抛 future_errorfuture 只允许取一次先share()再分发给多方数据竞争告警手写共享变量绕过通道一律走 promise/future 传递别绕过通道任何绕过共享状态的手写共享变量都会让上面的保证全部失效。先查生命周期局部 promise 被线程捕获后提前析构是最隐蔽的断链方式。核心结论异步卡死几乎都能追到“谁负责 set、谁负责等”这两个问题上。十、五件套选型一张对照表定方案五件套各有分工按下面这张表选就不会错需求首选备选关键原因一行启动拿结果std::asyncthread promise自动打包与回传手动控制写入时机promise/futureasync写入点可拆成多步任务队列 / 线程池packaged_task手写回调执行与回传绑定一写多读广播shared_future每人一个 future共享同一份结果带超时的等待wait_for future条件变量状态明确、易退出组合拳最常用async起任务 →share()广播 →wait_for()兜底超时。别混用通道同一条链路统一用一种传递方式日志与排错都简单。A B Casync 启动 share 广播 wait_for 超时兜底。结语异步编程里最难的从来不是“怎么开线程”而是“结果怎么安全地回来”。把 promise 当写端、future 当读端用 async 省掉样板代码用 packaged_task 支撑任务队列用 shared_future 覆盖广播再用 wait_for 加上超时兜底整条链路的写入点、读取点、异常出口和退出条件就都明确了。这套方案全部来自 C 标准库不需要引入第三方库也不需要手写条件变量与锁移植到任何支持 C11 及以上的工程都能直接使用。想清楚谁 set、谁 get、等多久异步结果自然回得来。