Future、Promise 与 Async / Futures, Promises, and Async
1. 核心是共享状态
producer shared state consumer
promise / packaged_task ----> [value | exception | ready] ----> futurepromise 和 future 不是直接互相调用;二者通过共享状态通信。生产者设置一次结果/异常,消费者等待并获取。
2. future 的一次性语义
std::promise<int> promise;
std::future<int> result = promise.get_future();
std::thread worker([p = std::move(promise)]() mutable {
try { p.set_value(compute()); }
catch (...) { p.set_exception(std::current_exception()); }
});
int value = result.get();
worker.join();future 只移动,get() 通常只能调用一次:它等待、取走结果,若共享状态保存异常则在调用线程重新抛出。valid() 表示是否关联共享状态,不表示结果已就绪。
3. promise 错误状态
- 重复
get_future()、重复设置值/异常会抛future_error; - promise 在未设置结果时销毁,会让共享状态成为
broken_promise,future 的get()抛异常; set_value_at_thread_exit等待线程局部对象清理后才标记 ready;- promise 自身不是任意多生产者队列,只对应一个结果槽。
4. packaged_task
packaged_task<R(Args...)> 把 callable 与共享状态绑定:
std::packaged_task<int(int)> task([](int x) { return x * x; });
auto future = task.get_future();
std::thread worker(std::move(task), 7);
worker.join();
assert(future.get() == 49);它是 move-only,调用时捕获返回值/异常。reset() 为同一 task 建立新的共享状态,但旧 future 仍关联旧状态。
5. std::async 启动策略
auto result = std::async(std::launch::async, compute);launch::async:异步执行函数;不应把“必定创建全新 OS 线程”写成接口保证;launch::deferred:直到get/wait时在等待线程惰性执行;- 默认允许实现选择二者之一,时序和并发度可能不同。
由 async 策略创建且尚未就绪的 future,在某些临时对象析构场景会阻塞。这会让看似并行的连续语句串行:应保存 future 并明确等待点。
6. shared_future
future::share() 转换为可复制 shared_future,多个消费者可等待和多次读取同一结果:
std::shared_future<Config> config = load_async().share();共享 future 对象可由多个线程操作,但返回的 const T& 指向同一个 T;若 T 内部可变,多线程访问仍需同步。
7. 等待、轮询与 deadline
wait():只等待,不取结果;wait_for/wait_until:返回 ready/timeout/deferred;- deferred future 的等待状态需要单独处理;
- 轮询间隔过短会浪费 CPU,过长会增加延迟;优先事件驱动或统一调度器。
超时不取消底层任务。future 标记 timeout 后,计算可能仍在运行并访问资源;需要独立的取消协议。
8. 缺少 continuation 的边界
C++11—23 的 std::future 没有统一标准 .then() continuation,组合多个 future 容易阻塞线程。复杂异步图可使用协程任务库、执行器框架或成熟并发库,但仍需定义所有权、取消和调度位置。
9. 异常与无人观察
若任务异常被存进共享状态却从未调用 get(),错误可能无人观察。任务系统应决定:强制消费 future、记录 detached 任务异常,或提供结构化作用域统一 join。
不要在 promise 设置失败后静默吞掉异常;至少让消费者得到 broken/error 状态。
10. 面试速答
问:future 与 promise 的关系?
它们分别是共享状态的消费者和生产者句柄,通过该状态传递一个结果或异常。
问:默认 std::async 一定开线程吗?
不一定,默认策略可选择 async 或 deferred;需明确并发时写 launch::async。
问:future 超时会取消任务吗?
不会,只表示等待在 deadline 前未 ready,底层任务仍可能运行。
11. 自测题与答案
valid()是否表示结果 ready?- promise 未设置结果就销毁会怎样?
- shared_future 为什么仍不能保证结果对象线程安全?
- packaged_task 与 promise 的主要区别?
<details> <summary>参考答案</summary>
- 否,只表示有关联共享状态。
- 状态变为 broken_promise,消费者 get 时收到 future_error。
- 它只让共享状态可被多消费者等待,T 的普通可变成员仍需同步。
- packaged_task 自动执行 callable 并捕获结果/异常;promise 由调用方手动设置。
</details>
12. 共享状态把生产者与消费者解耦
promise / packaged_task / async
|
v
shared state [empty -> value | exception | broken]
^
|
future waits/getsfuture 与 promise 是两个端点;数据不必存放在任一端点对象内部。promise 析构而未设置结果时,共享状态记录 broken_promise,使消费者不会永久等一个静默消失的生产者。
13. get 同时完成等待与结果消费
普通 future 是 move-only、一次性结果通道。get() 等待就绪、返回值或重抛异常,并使 future 不再拥有可再次获取的结果。shared_future 可复制并多次观察,但返回引用时要关注共享值并发访问。
14. packaged_task 把 callable 与 promise 协议绑定
callable + packaged_task
| get_future
consumer future
|+queue stores/moves task -> worker invokes -> state ready它适合线程池把任意任务执行结果接入 future;任务对象本身通常 move-only,队列类型擦除需支持这一点。
15. std::async 最大陷阱是启动策略
未明确策略时实现可选择 async 或 deferred。deferred 任务在首次等待/获取线程中同步执行:
async policy: caller ----> runtime thread executes
deferred : caller calls get -> same waiting thread executes若程序依赖并行,应显式 std::launch::async,但标准 async 仍不是可控线程池接口。临时 future 的析构在某些 async 情况会等待,连续表达式可能意外串行化。
16. 异常只有在结果被观察时才重新出现
生产者捕获异常存入共享状态,消费者 get() 时重抛。若 future 被丢弃,异常可能无人观察;任务系统应定义未观察错误的日志/监督策略。
17. 等待不是 continuation
标准 future(到 C++23 常用接口)缺少统一 then 组合,链式任务常需要额外线程阻塞等待或自己调度。不要为每个依赖创建一个“只等待 future 的线程”;在线程池中这还可能占满所有 worker:
all workers wait for tasks queued to same pool -> starvation/deadlock可使用支持 continuation 的库、协程 task 抽象,或显式依赖图调度。
18. deadline、取消与 future 是不同协议
wait_until 超时只表示调用者停止等待,不会自动取消生产者。标准 future 没有通用取消;需要额外 stop_token、任务状态与底层操作协作:
deadline expires -> request_stop -> operation acknowledges -> state completes cancelled19. 连续追问与自测
问:promise 与 future 谁拥有线程? 答:都不必拥有;它们只连接共享结果状态,执行位置由调用者/async/task 框架决定。
问:future 超时后任务会停止吗? 答:不会自动停止,需要单独取消协议。
- broken_promise 的作用? 答:生产端消失未交付结果时让消费端得到明确错误。
- shared_future 是否让结果对象线程安全? 答:不自动;共享状态访问安全不等于返回对象可变操作安全。
- deferred 何时执行? 答:等待/get 触发时在调用等待的线程中执行。
9. 真实问题:超时返回后后台任务仍在写状态
调用者对 future 使用 wait_for。 超时后接口返回“暂时不可用”。 但生产任务仍可能继续写缓存或数据库。 future 的超时只描述观察者没有等到结果。 它不包含取消语义。 调用者必须区分“停止等待”和“停止执行”。 任务接口要单独传递 stop_token 或取消句柄。 结果对象的生命周期也必须覆盖后台写入。 不要把 future 当作线程拥有者。
10. 最小实验:异常位于共享状态
std::promise<int> promise;
auto future = promise.get_future();
std::thread worker([p = std::move(promise)]() mutable {
try { throw std::runtime_error("bad input"); }
catch (...) { p.set_exception(std::current_exception()); }
});
worker.join();
try { future.get(); } catch (std::exception const& e) { std::cout << e.what(); }异常不会穿越线程栈。 它被存储进共享状态,get 时在消费线程重新抛出。 promise 只能设置一次结果。 重复 set_value 会抛 future_error。 promise 在未交付前析构会让 future 收到 broken_promise。 future 的 get 通常只能调用一次。 shared_future 才支持多个消费者读取共享结果。
11. async 启动策略
std::launch::async 要求异步执行,资源由实现管理。 std::launch::deferred 延迟到等待操作时执行。 默认策略允许实现选择,可能导致并发度不可预测。 高延迟任务应明确策略并设置资源上限。 future 析构对某些 async 任务可能等待完成。 这会在临时对象表达式中产生隐式阻塞。 把 future 保存到命名变量能让等待点更清晰。 不要在持锁期间调用可能等待的 get。
12. packaged_task 与任务封装
packaged_task 把可调用对象绑定到 future。 调用 packaged_task 才真正执行工作。 它适合线程池把任务和结果连接起来。 移动 packaged_task 可以转移执行权。 未调用的 packaged_task 销毁会使 future 失败。 任务包装不提供队列、重试和取消策略。 异常由任务调用过程写入共享状态。 返回大对象时考虑移动和共享所有权成本。
13. 工程实践:future 契约
接口文档写明结果可获取次数。 写明超时是否只停止等待。 写明异常类型和 broken_promise 的含义。 写明 future 析构是否可能阻塞。 避免返回引用指向短生命周期数据。 让共享状态只承载结果,不承载隐式全局状态。 需要多个观察者时尽早转成 shared_future。 为取消和超时分别记录指标。 对长任务提供进度或心跳,避免只能等待最终结果。
14. 失败场景复盘
失败一:超时后立即销毁上下文,异步任务继续使用指针。 修复:共享所有权或先请求停止并等待确认。 失败二:两个消费者都调用同一个 future.get。 修复:使用 shared_future 或集中分发结果。 失败三:promise 代码有多个返回分支,遗漏 set_value。 修复:使用 scope guard 或统一结果出口。 失败四:默认 async 策略在压力下变成 deferred。 修复:显式指定 launch 策略并测量。 失败五:在锁内 get 导致锁等待环。 修复:锁外等待,或先复制状态再等待。
15. 自测题
问:wait_for 超时会取消任务吗?答:不会。 问:future 能否跨线程复制?答:future 不可复制,shared_future 可复制。 问:promise 未设置值会怎样?答:消费端得到 broken_promise 异常。 问:deferred 在哪个线程运行?答:执行等待操作的线程。 问:async 是否等价于创建一个裸 thread?答:不是,执行和共享状态由库管理。
16. 等待策略和调用链
同步等待会占住调用线程。 在请求处理线程中无限等待,会耗尽服务端并发槽位。 在 GUI 线程中等待,会让事件循环停止响应。 在 worker 中等待同池任务,会形成线程饥饿。 因此先问等待发生在哪个执行域。 短等待可以配合 deadline。 长等待应该转换为回调、继续任务或状态机。 等待前释放不再需要的锁。 等待后重新验证缓存和关闭状态。 future 的 ready 不代表结果满足业务新鲜度。 超时结果也应记录任务标识和 deadline。
17. 实现边界
promise/future 的共享状态是库实现细节。 不要跨动态库假设其内存布局或异常 ABI 完全一致。 公共插件接口更适合 C ABI、稳定句柄或明确序列化格式。 跨模块传播 exception_ptr 会依赖异常运行时兼容性。 同一工具链和运行库通常可用,但发布契约仍应写清楚。 不要把 std::future 直接作为长期持久化任务协议。 进程重启后共享状态不存在。 分布式任务需要可恢复的任务 ID 和结果存储。 本地 future 适合进程内一次性协调。
18. 更完整的任务协议
任务提交时记录输入、deadline 和取消源。 执行端报告 started、progress、finished 三类事件。 结果状态只在一个终态中完成。 终态包括 value、exception 和 cancelled。 调用方超时后可以保留句柄稍后查询。 取消请求和取消确认是不同事件。 重复请求取消应当幂等。 结果交付后再取消通常无效但不应破坏状态。 这种状态机比“一个 future”更接近生产服务。 future 仍可作为终态通知通道。
19. 失败演练
安排一个任务故意 sleep 超过 deadline。 验证调用方超时后没有释放被借用的内存。 再请求停止并等待 worker 确认退出。 安排 promise 析构而不写结果。 验证消费端明确收到 broken_promise。 安排任务抛异常。 验证异常包含上下文并在预期边界报告。 安排 deferred 任务。 验证它没有悄悄在后台消耗线程。 这些测试比只断言返回值更能暴露协议缺口。
20. 这一节先记住
future 传递的是一次终态,不是取消控制器。 超时只结束本次等待。 异常必须被生产者写入共享状态。 等待点要避开锁、UI 线程和同池饥饿。 默认 async 策略不适合依赖确定并发度的服务。 任务真正的生命周期由拥有者和停止协议定义。
21. 暂时不用记
不必背诵每个 future_error 枚举值。 不必一开始就手写 continuation 框架。 先能区分执行、等待、取消和结果四件事。 当调用链变长时,再引入更高层任务运行时。
工程深化:把本篇知识落到项目里
这一节不是为了凑篇幅,而是把《Future、Promise 与 Async / Futures, Promises, and Async》从“知道概念”推进到“能在项目里稳定使用”。 阅读时可以把每个知识点都追问成四件事:它保护什么边界,失败时有什么现象,怎样最小复现,怎样写进团队流程。
concept -> boundary -> failure signal -> minimal proof -> project rule工程切片 1:最小可复现样例
**场景。**围绕 future 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。
**边界。**先判断这里讨论的是 future、promise 还是 异常传播 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> future -> promise -> 异常传播 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 2:接口边界
**场景。**围绕 promise 建一个很小的案例,不要一开始就放进完整业务系统。写清调用方能依赖什么、不能依赖什么,并把隐式假设转换成命名函数、配置项或测试。
**边界。**先判断这里讨论的是 promise、异常传播 还是 任务生命周期 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> promise -> 异常传播 -> 任务生命周期 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 3:失败注入
**场景。**围绕 异常传播 建一个很小的案例,不要一开始就放进完整业务系统。故意制造一个常见错误,让日志、断言或测试先失败,再用修复后的证据说明规则生效。
**边界。**先判断这里讨论的是 异常传播、任务生命周期 还是 取消边界 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 异常传播 -> 任务生命周期 -> 取消边界 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 4:跨平台差异
**场景。**围绕 任务生命周期 建一个很小的案例,不要一开始就放进完整业务系统。至少比较 Windows、Linux 或不同编译器下的一个差异,记录差异属于标准、实现还是平台约定。
**边界。**先判断这里讨论的是 任务生命周期、取消边界 还是 等待策略 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 任务生命周期 -> 取消边界 -> 等待策略 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 5:性能观察
**场景。**围绕 取消边界 建一个很小的案例,不要一开始就放进完整业务系统。用小规模和压力规模各跑一次,区分算法成本、同步成本、I/O 成本和工具链配置成本。
**边界。**先判断这里讨论的是 取消边界、等待策略 还是 future 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 取消边界 -> 等待策略 -> future -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 6:生命周期
**场景。**围绕 等待策略 建一个很小的案例,不要一开始就放进完整业务系统。标出资源创建、转移、共享、停止和销毁的顺序,特别关注错误返回和提前退出路径。
**边界。**先判断这里讨论的是 等待策略、future 还是 promise 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 等待策略 -> future -> promise -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 7:诊断证据
**场景。**围绕 future 建一个很小的案例,不要一开始就放进完整业务系统。保留命令、日志、符号、栈、测试输出或截图,让结论可以被另一个环境重新验证。
**边界。**先判断这里讨论的是 future、promise 还是 异常传播 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> future -> promise -> 异常传播 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 8:维护策略
**场景。**围绕 promise 建一个很小的案例,不要一开始就放进完整业务系统。把一次性经验沉淀到 README、CI、脚本、示例工程或检查表里,避免只存在个人记忆中。
**边界。**先判断这里讨论的是 promise、异常传播 还是 任务生命周期 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> promise -> 异常传播 -> 任务生命周期 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 9:版本演进
**场景。**围绕 异常传播 建一个很小的案例,不要一开始就放进完整业务系统。为未来变更预留兼容策略:版本号、特性位、弃用窗口、迁移脚本和回滚入口。
**边界。**先判断这里讨论的是 异常传播、任务生命周期 还是 取消边界 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 异常传播 -> 任务生命周期 -> 取消边界 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 10:最小消费者
**场景。**围绕 任务生命周期 建一个很小的案例,不要一开始就放进完整业务系统。从使用者角度写一个小程序或小工程,只依赖公开接口,验证安装包、头文件和运行时行为。
**边界。**先判断这里讨论的是 任务生命周期、取消边界 还是 等待策略 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 任务生命周期 -> 取消边界 -> 等待策略 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 11:边界复盘
**场景。**围绕 取消边界 建一个很小的案例,不要一开始就放进完整业务系统。用一张表说明问题发生在源码、编译、链接、加载、运行、关闭或发布中的哪一层。
**边界。**先判断这里讨论的是 取消边界、等待策略 还是 future 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 取消边界 -> 等待策略 -> future -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
本篇收束
掌握《Future、Promise 与 Async / Futures, Promises, and Async》的标志,不是记住所有名词,而是能把 future、promise、异常传播、任务生命周期、取消边界、等待策略 放进一条可验证的工程链路。 先用最小样例建立判断,再用测试和脚本固定判断,最后把失败证据留给未来的自己和团队。