C++ std::future 异步编程:核心原理、常见坑位与实战指南

发布时间:2026/10/21 18:37:47
C++ std::future 异步编程:核心原理、常见坑位与实战指南 最近在梳理异步编程相关的东西发现很多写了不少年代码的人对std::future的理解还停留在“从std::async里拿返回值”这个层面。真到要处理复杂并发任务、做超时控制、跨线程传异常的时候就开始各种踩坑。这玩意儿看着简单实际上里面不少细节很磨人。这篇东西不打算面面俱到讲语法重点放在“为什么是 future”“实际使用时它有哪些反直觉的行为”“怎么才能用得明白”这几个方向上逻辑到坑位顺带把 promise、packaged_task 这些配套组件也串起来聊聊。1. 为什么需要 future从裸线程到异步结果1.1 裸线程时代的三座大山在 C11 之前想在 C 里做一件耗时的事情同时不阻塞主流程最直接的做法就是pthread_create或者 Windows 下的CreateThread然后手动管理一个“结果区域”。那个年代写并发普遍要处理三大痛点第一是结果传递。线程跑完了结果放哪只能提前给它一块共享内存或者一个简单的结构体指针线程函数往里填值。调用方什么时候读加锁轮询还是闭眼sleep几毫秒再看这两种方式要么写起来丑陋要么白白浪费 CPU。第二是异常传播。线程函数里一旦抛出异常不好意思默认行为就是直接调用std::terminate让整个进程原地去世。你想把异常传回给调用方没有任何语言层面的机制支持你这么做。第三是生命周期。调用方如果先退出工作线程还在写那块共享内存直接就是悬垂指针级别的未定义行为。反过来工作线程没跑完但调用方已经不需要结果了你还得想尽办法去“礼貌地”请求它退出不然就可能出现资源泄漏或者死锁。这三座大山本质上都在问同一个问题怎么安全地表达“未来某个时刻才会有结果”这件事。而std::future就是 C 官方给这个问题的一个答案。1.2 future 的设计意图结果占位符你可以把std::future理解成一个“结果的收据”。你去餐厅点了个菜服务员不会把厨师拉到你的餐桌旁边做菜而是给你一张单号告诉你“到时候凭这个号取菜”。std::future就是这张单号实际做菜的人可能是std::async开出的线程可能是某个std::promise在背后赋值也可能是某个std::packaged_task在任务队列里执行完后的产物。这张“收据”有几个内在属性值得留意。std::future虽然叫“未来”但它内部并不会真的去执行任何任务它只是个通信管道。真正的结果状态存储在一个双方共享的状态shared state里一端负责填充另一端负责读取。这个共享状态什么时候准备好future.get()就什么时候返回。没有准备好之前调用get()的线程会阻塞在那里而不是空转。并且它跟“收据”一样是一次性的。你把菜取走之后单号就失效了不能拿同一张单号去取两次菜。std::future只能调用一次get()如果连续调用两次标准库会直接抛std::future_error逻辑错误码是future_already_retrieved。这个设计在最初看起来有点烦等你在多线程环境里真正跑过一两次就会发现它其实是刻意逼你写清晰的消费逻辑。1.3 适用场景定位std::future并不是要替代所有的并发原语。你如果只是想加个锁保护共享变量那锁和atomic还是你的主力。future 适合的场景是那种任务之间有依赖、或者主流程需要等待某个/某些异步结果才能继续的并发模型。常见的有三类单发异步任务的返回值获取。比如启动一个后台任务做文件压缩主线程继续绘图压缩完成之后把结果拿回来展示。多个独立任务的结果聚合。比如并行下载多份数据都完成后再合并处理。线程间的信号通信。某些情况下你可以用std::promise做一次性的事件通知比条件变量更简洁因为不用维护额外的标志位。换句话说future 家族更擅长“结果导向”的并发而锁和条件变量更擅长“互斥与状态同步”的并发。很多工程里最别扭的写法就是明明只需要一个异步结果却硬生生引入了共享状态 互斥锁 条件变量代码又长又容易死锁。换成 future 之后清爽很多。2. std::future 核心用法拆解2.1 从 std::async 认识 futurestd::async是最容易上手的一套入口它是用来创建 future 的“快捷方式”。最基本的用法如下#include iostream #include future #include chrono int heavy_compute(int input) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); return input * input; } int main() { std::futureint result std::async(std::launch::async, heavy_compute, 12); std::cout 任务已经开始主线程可以做别的事... std::endl; std::cout 结果: result.get() std::endl; return 0; }这里std::async的第一个参数是启动策略第二个参数是可调用对象后面的参数会作为函数的实参传递进去。返回值就是一个绑定到共享状态上的std::futureint。用起来很简单但我要强调一个新手经常忽视的点std::async返回的 future 是热任务还是冷任务取决于你传的启动策略。如果传std::launch::async任务会立刻在另一条线程执行如果传std::launch::deferred任务会等到你调用get()或者wait()时才在当前线程里同步执行。2.2 部署策略抉择async 还是 deferredstd::launch这个枚举有两个典型取值对应两种执行时机std::launch::async保证在调用std::async时就开始在独立线程上执行。std::launch::deferred不启动新线程直到调用get()/wait()时才在调用线程中同步执行。你如果不写第一个参数直接std::async(func)标准库实现可以自由选择上述两种策略之一。这就导致了一个非常隐蔽的问题行为不确定。同样的代码在某些实现和平台上可能是并发的在另一些平台上可能变成串行。我曾经就在某次代码评审中见过一个同事写的服务他默认std::async一定会起新线程结果在某个环境下所有任务全都延迟到get()时才同步执行原本的异步优化彻底失效。所以我的建议非常明确一定要显式传入std::launch::async除非你故意想要 deferred 那种懒执行语义。不要依赖标准库的默认选择因为默认选择对程序的行为来说是不可预测的。什么时候用 deferred 呢其实也有实用场景。比如你希望任务对象创建得很便宜真正用到结果时才去计算有点像懒加载。但这种情况更推荐用std::call_once或者std::optional配合普通函数语义更直接。我对 deferred 策略的总体看法是它的存在主要是为了一些“延迟求值”的库内部设计日常应用层代码里能避就避。2.3 get、wait、wait_for 家族拿结果前先学会等std::future提供了一套查询和等待的方法实际工程里每个都对应不同的需求。get()阻塞等待结果就绪然后取走结果。这是唯一能拿到结果/异常的方式。调用后future 变为无效不能再取。wait()只等待结果就绪不取走结果。等价于跟共享状态握手告诉它“我知道你好了但我暂时不想拿”。wait_for(duration)最多等待一段时间返回三种状态之一std::future_status::ready、std::future_status::timeout、std::future_status::deferred。wait_until(time_point)同理只是用绝对时间点来限定等待截止时机。实际开发中wait_for可以说是使用频率极高的一个。比如你要从某个异步任务里拿结果但主流程最多允许等 100 毫秒超时就先走分支逻辑std::futureint result std::async(std::launch::async, heavy_compute, 7); if (result.wait_for(std::chrono::milliseconds(100)) std::future_status::ready) { std::cout 任务完成结果为: result.get() std::endl; } else { std::cout 任务尚未完成先处理其他事情... std::endl; }这里有一个非常容易踩的坑wait_for返回future_status::deferred时说明这个 future 对应的策略是 deferred它根本不会执行等多久也是 timeout。正确做法是先检查deferred然后决定是否需要手动调用get()触发同步执行。但前面说了推荐总是用std::launch::async这样就不会有这个分支了。2.4 当多个线程都要读结果shared_future原生std::future的get()是独占的只有一个线程能拿走结果。如果多个线程都想知道这个异步任务的结果呢你让每个线程各拿一个 future 变量其实是没用的因为每个 future 关联的不同异步任务。它们的共享状态是独立的。正确的姿势是用std::shared_future。std::promiseint prom; std::shared_futureint shared_result prom.get_future().share(); // 可以复制给多个线程 auto worker [shared_result] { try { int val shared_result.get(); std::cout 线程读到的值 val std::endl; } catch (const std::exception e) { std::cout 捕获异常 e.what() std::endl; } }; std::thread t1(worker); std::thread t2(worker); std::this_thread::sleep_for(std::chrono::milliseconds(300)); prom.set_value(42); t1.join(); t2.join();shared_future的get()可以被多个线程反复调用因为它本质上是读同一个共享状态而不是像future那样迁移结果所有权。这个组件在处理“广播型结果发布”的场景时特别好用比如多个工作线程都需要等待某个全局配置加载完成然后同时开始处理数据。不过有一点要说明std::shared_future内部为了保证并发读安全是有加锁开销的。如果只是单线程读用它反而多了一层无谓的成本不应滥用。3. promise 与 packaged_task手动掌控异步事件3.1 std::promise把结果交出去的承诺std::future提供的是“读”的视角那“写”的视角在哪里答案是std::promise。它是一个写入端与 future 共享同一个状态。你可以理解成std::promise的set_value()就是把菜端出来的那个动作而未来持有 future 的一方会在get()处等到这盘菜。典型的使用场景你有一个工作线程但这个线程不是由std::async启动的而是你自己用std::thread管理的。你想让它算完后把结果传出来又不想引入锁 条件变量的组合。此时 promise 就非常合适。#include iostream #include future #include thread void producer(std::promiseint prom) { int result 100; // 模拟一些计算 std::this_thread::sleep_for(std::chrono::milliseconds(200)); prom.set_value(result); } int main() { std::promiseint prom; std::futureint fut prom.get_future(); std::thread t(producer, std::move(prom)); std::cout 等待生产者线程... std::endl; int value fut.get(); std::cout 收到结果: value std::endl; t.join(); return 0; }注意这里promise必须用std::move传给线程。因为std::promise不可拷贝它内部引用的 shared state 只能有一个写端。set_value还有个容易迷惑的点它只能调用一次。如果某个 promise 的写端已经设置过值再调用一次set_value就会抛std::future_error。如果是这样需要避免重复设置。在实际代码里我见过好几个人在这里踩坑一个 promise 在多个线程里被同时设置因为共享状态对象的写端只能存在一个往往被 move 走了之后源对象就处于 no-state 状态调用set_value会直接抛no_state异常。所以正确做法是promise 的所有权明确设置值的动作必须严格由持有者完成不要拷贝不要跨线程共享同一个 promise。3.2 std::packaged_task可打包的异步任务std::promise的粒度比较低它要求你自己处理函数调用的逻辑。那如果我手里的就是一个可调用对象想让它执行完自动把结果填进 future 呢std::packaged_task就是干这个的。你把它理解成一个“自带未来结果的函数包装器”。它把函数和 promise 绑定在一起函数执行完成后返回值自动传给 future函数抛出的异常也会自动捕获并放进 future。#include iostream #include future #include thread int add(int a, int b) { return a b; } int main() { std::packaged_taskint(int, int) task(add); std::futureint fut task.get_future(); std::thread t(std::move(task), 10, 20); std::cout 任务结果: fut.get() std::endl; t.join(); return 0; }packaged_task跟 promise 最大的区别是它接管了“函数调用”这个过程本身。你设置好参数它的operator()一旦被调用就会执行内部函数并把结果写入共享状态。这种设计很适合做任务队列。你可以把一系列packaged_task放进队列由一组工作线程取出来执行。主线程持有每个任务对应的 future等待结果。实际编码时有几个细节要注意。std::packaged_task本身也是不可拷贝的但可以被 move所以放进容器时要用std::move。而且如果packaged_task对象在调用operator()之前就被析构了它的 future 会处于“无值且无异常”的状态这时候调用get()会抛broken_promise异常。这个broken_promise是我后面要重点说的一个坑位。3.3 异常传递机制try/catch 跨线程future家族一个非常友好的设计是异常也会被包装进共享状态。你在工作线程里抛出std::runtime_error调用get()的线程不需要在异常发生的线程里处理而是在自己这边重新抛出。#include iostream #include future #include stdexcept int risky_task() { throw std::runtime_error(something went wrong inside the thread); return 1; } int main() { std::futureint fut std::async(std::launch::async, risky_task); try { int x fut.get(); std::cout 不会走到这里: x std::endl; } catch (const std::exception e) { std::cout 在主线程捕获到异常: e.what() std::endl; } return 0; }从实现机制上讲std::promise::set_exception(std::current_exception())就是干这个的但在std::async和packaged_task里它是自动完成的不需要你手工写。当get()遇到一个保存了异常的 shared state 时就会把这个异常重新抛出给调用者。这个特性非常实用它让跨线程的错误处理回归到熟悉的 try/catch 模式。不过我建议时刻记住异常如果一直没人去get()它就一直积压在 shared state 里不会自己消失。如果程序不需要结果但任务可能抛异常一定要调用wait()和get()即使抛掉否则 shared state 里存放的异常对象可能长期占用资源。4. 实操完整异步任务调度示例4.1 场景设定与总体设计纸上谈兵聊了这么多我直接给一个比较典型的真实场景假设你需要对一个数组的每个元素做一个较耗时的变换然后汇总结果。如果直接在主线程里循环处理会卡住界面或请求处理如果为每个元素各开一条线程资源消耗又太高。更合理的做法是启动一个小型线程池把任务拆成 N 份并行处理最后由主线程聚合 future 结果。这里我刻意不用第三方线程池库就用手边的原语来演示方便你看清楚背后的数据流。首先定义任务函数#include iostream #include vector #include future #include thread #include numeric int heavy_transform(int x) { // 模拟耗时操作 std::this_thread::sleep_for(std::chrono::milliseconds(100)); return x * x; }然后我们构造一个需求有一个大小为 16 的数组每个元素都执行heavy_transform。为了控制并发度我用固定数量的工作线程从一个队列里取任务执行。但标准库没有线程池队列怎么办我们可以用std::packaged_task来做到这一点。4.2 基于 packaged_task 的简易任务队列设计思路把每个任务封装成std::packaged_taskint()然后把它的 future 保存在一个 vector 里。工作线程从队列里取出 task 并执行。主线程持有所有 future最后统一get()。#include deque #include mutex #include condition_variable #include functional class TaskQueue { public: void push(std::packaged_taskint() task) { std::lock_guardstd::mutex lock(m_mutex); m_tasks.push_back(std::move(task)); m_cv.notify_one(); } std::packaged_taskint() pop() { std::unique_lockstd::mutex lock(m_mutex); m_cv.wait(lock, [this] { return !m_tasks.empty() || m_stop; }); if (m_stop m_tasks.empty()) { return std::packaged_taskint()(); } std::packaged_taskint() task std::move(m_tasks.front()); m_tasks.pop_front(); return task; } void stop() { std::lock_guardstd::mutex lock(m_mutex); m_stop true; m_cv.notify_all(); } private: std::dequestd::packaged_taskint() m_tasks; std::mutex m_mutex; std::condition_variable m_cv; bool m_stop false; };工作线程逻辑void worker(TaskQueue queue) { while (true) { std::packaged_taskint() task queue.pop(); if (!task.valid()) { break; } task(); // 执行任务结果自动写入 associated future } }在主函数中构建任务int main() { std::vectorint inputs(16); std::iota(inputs.begin(), inputs.end(), 1); TaskQueue queue; const unsigned int thread_count 4; std::vectorstd::thread threads; for (unsigned int i 0; i thread_count; i) { threads.emplace_back(worker, std::ref(queue)); } std::vectorstd::futureint futures; for (int x : inputs) { std::packaged_taskint() task([x] { return heavy_transform(x); }); futures.push_back(task.get_future()); queue.push(std::move(task)); } int total 0; for (auto fut : futures) { total fut.get(); } std::cout total total std::endl; queue.stop(); for (auto t : threads) { t.join(); } return 0; }这个例子最核心的地方在于任务的产生端和消费端完全解耦packaged_task负责把每个任务的“结果去向”提前固定好工作线程执行完只是往 shared state 里写结果主线程按顺序从 future 里取即可。这种设计有几个好处并发度可控线程数固定为 4不会因为任务数多而无限增长线程。结果对应关系清晰哪个 future 对应哪个输入一目了然不会有竞态条件。不需要锁来保护数据结果每个任务的结果在自己的 shared state 里天然隔离。当然这个任务队列实现里条件变量那块要仔细检查pop里m_stop是在锁内读取的但线程需要被 notify 唤醒。实际运行中stop()调用后pop会通过m_stop判断退出这是 OK 的。4.3 参数与资源开销分析很多人在设计并发任务时会忽略一个事实future 和 packaged_task 内部的 shared state 是有堆内存开销的。任务数量非常大的时候每个任务都保存一个 future就是 N 份 shared state。如果你只是想把一个 range 切分给几个线程处理那完全没必要为每个元素都建 future拆成几段每段一个 future 就好。我拿上面的例子说inputs 是 16 个元素每个元素都建 future那 shared state 就有 16 个。如果改成 4 个线程每个线程处理 4 个连续元素只需要 4 个 future开销显著下降而且代码思考成本也低一些。实际工程里到底是“每个任务一个 future”还是“每个线程一个 future”取决于任务的粒度和数量。粒度细、数量多一定要用线程池或者批量提交接口不要无脑堆 future。粒度粗、数量少直接 future 反而更直观。4.4 一个容易忽略的问题等待顺序与死锁上面这个例子里主线程是按 futures 的顺序逐个get()的。这意味着如果第一个任务的执行时间很长即使后面几个任务已经完成主线程也会卡在第一个get()上。在某些场景下这不是问题因为主线程本来就打算“收集所有结果之后再继续”。但如果你希望主线程“哪个任务先完成就先处理哪个”那么按顺序get()就不合适了。解决办法可以是让每个任务完成的时候把结果塞到一个线程安全的队列里或者用wait_for轮询所有 future看哪些已经 ready。后者在 future 数量多的时候不够高效但不失为一种简单可行的方案。而真正需要警惕的是线程池的停止顺序和 future 的等待顺序之间有死锁风险。如果工作线程执行任务时任务内部又去等待另一个 future而这个 future 恰好需要线程池里另一个工作线程才能完成而线程池已经满员就可能死锁。这也是为什么高阶并发编程不建议在池内任务里随意阻塞等结果的原因。5. 常见问题与排查技巧实录5.1 future 析构阻塞问题最隐蔽的坑我第一次遇到这个问题的时候花了好几个小时排查。代码长这样std::futurevoid maybe_log() { return std::async(std::launch::async, [] { std::this_thread::sleep_for(std::chrono::seconds(3)); std::cout log printed std::endl; }); } void process() { maybe_log(); // 返回值没接住直接弃置 // 继续做别的事 }你猜怎么着process()会停在那里 3 秒。原因是std::async返回的 future 析构时有一个特殊规则如果这个 future 引用的 shared state 尚未就绪析构函数会阻塞等待任务完成。换句话说你如果把 future 直接丢弃它会变成同步等待。这个行为的标准依据是 [futures.state] 里的相关条款。简单理解std::future的析构函数会“耐心等待”关联任务完成以避免任务写一个已经销毁的 shared state。但是对由std::async产生的 future这个等待是强制性的。解决办法很简单如果你确实需要异步但不关心结果就不要用std::async。可以用std::threaddetach但要格外小心生命周期或者把任务提交给你自己实现的线程池。如果你用std::async就一定要把 future 保存下来并最终调用get()或者wait()。其实这种行为也算合理它保护了任务不会写一个悬垂状态。但如果你不知道这一点第一次碰到时绝对一头雾水。特别是在代码里看到有人写std::async(...)却随手扔掉返回值那就是在制造隐藏的性能陷阱。5.2 broken_promise 是什么回事我在前文提过broken_promise这是使用 future 时会遇到的经典异常。它的触发场景是promise 对象在还未set_value/set_exception的情况下被销毁了或者packaged_task 尚未来得及执行就被析构了。这时候 future 端会共享到一个“没有结果”的异常状态get()就会抛出std::future_error错误码是broken_promise。实际案例某业务里我创建了一批packaged_task放进任务队列但某条任务因为某些校验条件没有入队而是直接被丢弃。于是那个 future 就永远不可能有结果了。等到消费端去get()就爆出了broken_promise。排查这种问题建议从几个方向入手检查任务是否被正确执行是否在某些分支里被提前丢弃。检查 promise/packaged_task 是否发生了不必要的移动操作导致源对象在空壳状态下被销毁。检查程序是否有异常被捕获后吞掉导致set_exception没有被调用。说白了broken_promise本质上是在告诉你一个承诺失去了兑现者。遇到它优先去查这个 future 对应的写端是不是已经“跑路”了。5.3 get() 之后为何报 future_error还有一种很常见的错误是同一个 future 调用了两次get()。第一次get()把结果取走第二次会抛std::future_error错误码是future_already_retrieved。这个错误通常出现在代码重构后原本只调一次get()后来有人拿着 future 到两个地方去取值或者在循环里调用get()。解决方案要么改成std::shared_future如果多线程都要读要么在代码里保证只调用一次。检查自己是不是踩了这个坑可以直接查future.valid()。如果get()已经被调用过valid()返回 false。标准库专门提供了这个方法就是为了让你在调用get()前先确认状态。5.4 wait_for 永远超时deferred 策略的恶作剧我在 2.2 里说过std::async不带启动策略时可能选择 deferred。这种情况下wait_for()会立即返回future_status::deferred不会执行任何任务。如果你代码里写的是while (fut.wait_for(std::chrono::milliseconds(100)) ! std::future_status::ready) { // 干别的事 }那这个循环可能永远不会退出因为 deferred 任务根本没开始跑。解决方式还是那句话显式用std::launch::async。如果你已经用了std::launch::asyncwait_for依然超时那就单纯是任务太慢了属于正常业务判断。5.5 析构顺序与线程生命周期隐患有一个容易出问题的模式是这样的std::futureint f; { std::promiseint p; f p.get_future(); } // 这里 p 析构f 变成 broken_promisepromise 一旦离开作用域就被销毁future 端拿不到任何值get()抛出broken_promise。这看起来很自然但真有不少同事在实现“延迟设置结果”的类时踩到。正确的设计是promise 作为成员变量存在或者用shared_ptrstd::promiseint共享给异步任务确保写端活到了set_value之后。另一个相关的情况是用std::thread执行任务时线程对象必须先join()或detach()再销毁否则std::thread的析构会直接std::terminate。future 本身不管理线程生命周期它管的是结果状态这两者要分开理解。5.6 实战排查思路遇到 future 相关的 bug我通常按下面的顺序排查先看异常类型。std::future_error会带一个error_code根据std::future_errc枚举可以快速定位broken_promise、future_already_retrieved、promise_already_satisfied、no_state。这一步就能区分很多问题。再看是否持有 future 的线程跟持有 promise 的线程生命周期匹配。谁先死promise 先死就是broken_promisefuture 先死且 async 任务没跑完就是阻塞析构。看是不是同一个 future 被多线程访问。std::future本身不是线程安全容器它的get()不允许两个线程同时调用。多线程读结果必须用std::shared_future。看任务队列/线程池的设计。任务内部不能无限等待其他任务池子大小和任务依赖要一起设计。6. 与线程池、actor 模型的结合思路6.1 future 与线程池的边界future 并不是线程池的替代品。线程池解决的是“任务如何在多个线程上高效调度”的问题future 解决的是“任务结果如何在生产者和消费者之间传递”的问题。两者经常组合使用线程池执行packaged_task让每个任务的 future 对外暴露调用方按需等待。不过要记住一个设计经验线程池的任务最好是“运行到完”的。如果一个池内任务为了拿另一个任务的 future 而阻塞等待而这个另一个任务也被丢进同一个池子那就可能因为池子线程全部阻塞而导致死锁。future 的优雅之处在于它让“等待”这件事变得很明确而不是像裸条件变量那样纠缠不清但明确不等于消除风险。所以在一个有依赖关系的并发系统里我会优先考虑用“任务依赖图”或者“actor 模型”来组织数据流而不是简单地把有依赖的任务都扔进一个线程池。future 在这种场景里更像是局部组件之间的耦合器而不是全局调度器。6.2 任务取消与超时控制future 做不到的事future 能表达“等待结果”但它本身不能强制取消一个正在运行的任务。你调用wait_for(duration)超时后任务线程如果继续跑没人能阻止它。C20 引入的std::jthread和std::stop_token提供了一种协作式取消机制但也只是优雅地通知线程“你可以停了”并不是强杀。这一点务必要在项目设计阶段就想清楚future 适合的是“你不等它它也很快就会跑完”或者“你愿意等它跑完”的任务。如果你的业务流程里明确要求“1 秒跑不完就取消并回滚”单单靠 future 是不行的你要么用协程配合超时取消要么自己设计协作式取消协议。我之前在一个文件导出服务里就面临过这个问题调用方设了 5 秒超时但导出任务跑了 20 秒。future 能告诉我“还没好”但它不能帮我“让它别跑了”。最后我是在任务内部周期性地检查一个原子布尔变量超时后由外部把这个变量置为 true任务内部看到后主动清理并退出。这才是一个可行的取消方案。6.3 C20/23 视角future 的进化方向C20/23 在异步模型上并没有对std::future做太多语法层面的革命但它们引入了std::jthread、std::stop_token等协作取消的基础设施而且协程提供了比 future 更自然的写法。我个人并不认为协程会完全取代 future。future 背后那套 shared state 模型仍然是协程异步落地时的重要组件。编译器把协程变成一个可恢复的任务时返回值往上层传递的通道往往是std::future或者std::experimental::future。所以我的建议是现有代码里的 future 用法不用推翻C 社区花了十年才把 future 的语义稳定下来它作为“结果传递”原语的地位依旧稳固。真正要迭代的是任务调度层和取消层那些更适合用协程和 stop_token。7. 几点经验心得踩过的坑多了之后我现在对 future 的使用有一套自己的默认原则。第一只要用std::async就显式加std::launch::async。省一次默认策略的犹豫成本换来行为可预期。第二future 要么保存下来马上消费要么就别用它异步执行。随手丢弃 future 等于把异步变成同步而且这个同步还是隐式的不仅浪费性能还会让代码看起来像“玄学卡顿”。第三跨线程传结果优先用 promise跨线程传函数优先用 packaged_task启动独立异步任务优先用 async。三者虽然能互通但在代码可读性上各有侧重。promise 更适合表达“某个事件发生后写入结果”packaged_task 适合表达“这个具体函数有一个 future 结果”async 则适合最简单直接的“起个任务”。第四所有会调用get()的地方都要想清楚异常策略。future 能把异常从工作线程传到主线程这是优点但也意味着你必须在get()附近准备好对应的 catch。如果异常没有及时处理它会一直留在 shared state 里如果这个状态生命周期很长异常对象也可能一直存活。第五不要把 future 当线程安全的信号量乱用。它确实可以在某些场景替代一次性的 condition_variable但只适合“一对多发布”或“一对一结果传递”。多个线程要同时阻塞在同一个 future 上用 shared_future多个线程要互斥地消费同一个结果考虑条件变量。回头看std::future的设计其实还挺诚实的它把并发编程里最重复、最琐碎的“结果传递与异常传播”抽象成了一个可读性很强的组件。虽然它不解决调度、不解决取消、不解决死锁但把那一道最常见的数据流管好以后你能腾出更多精力放在真正棘手的地方。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询