C++:future、promise、packaged_task 与 async
面试回答
常见问法
future、promise、packaged_task、async分别是什么?它们之间是什么关系?future::get()为什么通常只能调用一次?std::async和std::thread有什么区别?std::launch::async和std::launch::deferred有什么区别?promise怎么传返回值、异常?如果忘了设置结果会怎样?packaged_task和async的使用场景怎么选?
回答
这几个工具本质上都在解决一个问题:异步任务执行完以后,结果、异常、完成状态怎么安全地回到调用方。
它们的核心关系可以概括为一条链:
future:结果接收端promise:结果生产端packaged_task:把可调用对象包装成“执行后自动把结果写进共享状态”的任务async:标准库提供的高级异步启动接口,帮你把任务执行和结果返回串起来
它们背后通常都依赖一个共享状态(shared state),里面至少包含:
- 返回值
- 异常对象
- 是否完成的标记
一句话区分
thread解决的是:谁去执行future/promise解决的是:结果怎么回来async解决的是:既帮你执行,又帮你拿结果
典型回答
如果面试官问这四者关系,可以这样答:
future、promise、packaged_task、async都围绕“异步结果传递”展开。future是读端,负责等待并获取结果;promise是写端,负责手动设置结果或异常;packaged_task是把一个函数包装成任务,调用后会自动把返回值写到关联的future;async则是更高级的封装,既负责启动任务,又直接返回future。 它们底层都有共享状态,所以不仅能传值,也能传异常和完成状态。工程里如果只是想简单启动异步任务并拿结果,优先用async;如果要手动控制结果回传,或者线程和结果来源分离,就用promise;如果要把任务对象和 future 绑定起来,适合任务调度场景,就用packaged_task。
追问
- 为什么
future::get()只能调用一次?如果多个地方都要读结果怎么办? async默认策略安全吗?为什么面试里最好主动写std::launch::async?promise析构时如果还没设置结果会发生什么?async会不会一定创建新线程?packaged_task和普通 lambda +promise的本质区别是什么?future能不能做回调、继续链式 then?标准库原生支持强吗?
原理展开
1. 共享状态:这套机制的核心
这几类工具的共同基础是共享状态。可以把它理解成一个“结果邮箱”,异步任务执行完后,把内容投递进去;调用方拿着 future 去等这个邮箱。
共享状态一般包含:
- 结果值
- 异常
- 是否 ready
- 等待/同步机制
所以 future 不只是“拿返回值”,它其实是:
- 一个等待句柄
- 一个结果读取器
- 一个异常重抛入口
std::future<int> fut = std::async(std::launch::async, [] {
return 42;
});
std::cout << fut.get() << '\n'; // 阻塞直到结果就绪
这里 get() 做了两件事:
- 等待任务完成
- 取出结果;如果任务里抛异常,会在这里重新抛出
2. future:读结果,不负责执行
std::future<T> 本身不是线程,也不启动任务,它只是一个“将来能拿到 T 的对象”。
关键特性
get()会阻塞直到结果就绪wait()/wait_for()/wait_until()只等待,不取值get()通常只能调用一次future一般是独占读取,是 move-only,不可拷贝
为什么 get() 通常只能调用一次?
因为对 std::future 来说,get() 会把共享状态中的结果“取走”,调用后该 future 通常失效,不能再继续读。
std::future<int> f = std::async(std::launch::async, [] { return 10; });
int x = f.get(); // OK
// int y = f.get(); // 错误:future 已经没有可取结果了
如果多个消费者都要读结果怎么办?
用 std::shared_future。
std::future<int> f = std::async(std::launch::async, [] { return 99; });
std::shared_future<int> sf = f.share();
std::cout << sf.get() << '\n';
std::cout << sf.get() << '\n'; // 可重复读
面试要点
future是结果句柄,不是执行实体get()不仅取值,也负责异常传播- 需要多次读取时用
shared_future
3. promise:手动写结果的生产端
std::promise<T> 用来主动把结果或异常写入共享状态,再由对应的 future 读取。
最典型场景
- 工作线程和结果返回逻辑分开
- 任务不是一个简单函数返回,而是由外部事件驱动完成
- 你需要手动决定“什么时候算完成、返回什么、抛什么异常”
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread t([p = std::move(p)]() mutable {
p.set_value(100);
});
std::cout << f.get() << '\n';
t.join();
传异常
promise 不仅能 set_value,还能 set_exception:
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread t([p = std::move(p)]() mutable {
try {
throw std::runtime_error("something wrong");
} catch (...) {
p.set_exception(std::current_exception());
}
});
try {
std::cout << f.get() << '\n';
} catch (const std::exception& e) {
std::cout << e.what() << '\n';
}
t.join();
常见追问:如果 promise 提前析构且没写结果呢?
这会导致关联的 future 在 get() 时收到 std::future_error,错误原因通常是 broken promise。
这点特别能体现你对边界情况的理解:
promise必须最终把共享状态置为 ready,要么设置值,要么设置异常;如果生产端提前消失,消费者拿结果时会发现“承诺被打破”。
面试要点
promise是手动通信模型- 它不负责启动线程
- 适合线程/任务执行和结果回传分离的场景
- 别忘了异常路径也要
set_exception
4. packaged_task:把函数包装成“自动写结果”的任务
std::packaged_task<R(Args...)> 可以把一个可调用对象包装起来。
当你执行这个任务时,它会自动把返回值或异常写入关联的共享状态,然后由 future 去取。
理解重点
它相当于把:
- “执行函数”
- “把结果写进 future 对应共享状态”
这两件事绑定在一起。
std::packaged_task<int()> task([] {
return 123;
});
std::future<int> f = task.get_future();
std::thread t(std::move(task));
std::cout << f.get() << '\n';
t.join();
它和 promise 的区别
promise:你自己手动set_valuepackaged_task:函数执行完,结果自动写入
所以 packaged_task 常见于:
- 自己写线程池/任务队列
- 任务先入队,稍后由某个工作线程执行
- 既想保留“任务对象”的概念,又想拿
future
面试要点
如果面试官问 “那为什么不用 promise 就好了?”
可以这样答:
promise适合手动控制结果;packaged_task更适合“一个函数就是一个任务”的模型,因为它把执行和结果写回绑定好了,特别适合任务调度框架。
5. async:最高层封装,但别忽略启动策略
std::async 是标准库提供的高级接口。你传一个可调用对象进去,它返回 future,底层帮你建立共享状态,并决定任务何时执行。
std::future<int> fut = std::async(std::launch::async, [] {
return 42;
});
std::cout << fut.get() << '\n';
为什么说它是高级封装?
因为它相当于帮你做了几件事:
- 创建共享状态
- 包装任务
- 启动或延迟执行
- 返回
future
所以它是“最省事”的方式。
重点:启动策略(launch policy)
这是面试高频点。
std::launch::async
表示任务应以异步方式执行,通常意味着尽快在另一个执行上下文里运行。
auto f = std::async(std::launch::async, [] {
return work();
});
std::launch::deferred
表示任务延迟执行,直到你调用 get() 或 wait() 时才真正运行,而且通常由调用 get()/wait() 的线程执行。
auto f = std::async(std::launch::deferred, [] {
return work();
});
两者的核心区别
async:更像“现在就派出去做”deferred:更像“先记账,等你真要结果时再做”
默认策略要不要说?
一定要说。默认调用:
auto f = std::async([] { return 1; });
其策略通常等价于允许实现自己决定使用 async、deferred,或者两者之一。
也就是说:你不显式写策略,就把行为的一部分交给了实现。
面试中建议主动说:
我在工程里通常会显式指定启动策略,避免默认策略带来时机不确定、性能行为不透明的问题。
常被追问:async 会不会一定创建新线程?
不保证。 标准保证的是异步语义或延迟语义,不保证一定是“新建一个 OS 线程”,更不等于线程池。
这是很容易答错的点。高分回答应该说:
std::async关注的是任务启动语义,不是线程创建细节。它不是线程池接口,是否创建新线程由实现和策略共同决定。
6. 异常传播:这套机制的一个核心价值
这是面试里很加分的点。
因为线程函数如果直接抛异常,不按设计处理,问题会很严重;而 future/promise/async 这套模型,能把异常也当成结果的一部分传回来。
async 的异常传播
auto f = std::async(std::launch::async, []() -> int {
throw std::runtime_error("failed");
});
try {
int x = f.get();
} catch (const std::exception& e) {
std::cout << e.what() << '\n';
}
promise 的异常传播
std::promise<int> p;
auto f = p.get_future();
try {
throw std::runtime_error("error");
} catch (...) {
p.set_exception(std::current_exception());
}
面试要点
- 异常不是在线程里直接“自动回到主线程”
- 它是通过共享状态保存,再在
future::get()时重新抛出 - 所以
get()不只是“取值”,它也是“错误边界”
7. 工程实践:怎么选
场景 1:只想简单起个异步任务并拿结果
优先用 std::async
优点:
- 简洁
- 自带 future
- 自动处理异常传播
auto f = std::async(std::launch::async, [] {
return compute();
});
场景 2:任务执行和结果回传不是一个函数完成
用 promise + future
例如:
- 网络回调到了才算完成
- 某个线程等待外部事件再写结果
- 想自己控制成功/失败时机
场景 3:要把任务对象塞进队列,后面再由 worker 执行
用 packaged_task + future
这类场景最常见于:
- 自定义线程池
- 任务调度器
- 生产者消费者模型
场景 4:多个地方都要读同一个结果
用 shared_future
8. 面试里值得主动补充的边界细节
边界 1:future 不是可复制的
std::future 通常是 move-only,因为它表达的是独占接收权。
边界 2:promise::get_future() 一般只能调用一次
一个 promise 只对应一个结果通道,不能反复生成多个独占 future。
边界 3:packaged_task 也是单次结果模型
执行一次任务,对应一个共享状态;要复用通常需要重新准备任务或 reset() 后再建立下一轮使用。
边界 4:async 返回的 future 可能影响析构行为
这是高频追问点。
对于 std::async(std::launch::async, ...) 创建的任务,如果返回的 future 是最后一个引用,某些情况下它的析构可能会等待异步任务完成。
所以工程里不要随手丢弃 future,否则容易出现你没意识到的阻塞。
可以这么答:
async的 future 不只是一个结果对象,它还可能影响任务的同步时机,所以我一般不会忽略返回值,而是明确get()、wait(),或者说明我就是不关心结果。
边界 5:标准 future 能力其实不算强
原生 std::future 缺少很多现代异步框架常见能力,比如:
- 原生 continuation/then 链式操作弱
- 取消能力弱
- 组合多个 future 的能力有限
这不是必须答,但如果面试偏工程或中高级,很加分。
对比总结
future / promise / packaged_task / async 对比
| 概念 | 本质角色 | 谁负责执行任务 | 谁负责写结果 | 谁负责读结果 | 典型场景 | 优点 | 缺点 |
|---|---|---|---|---|---|---|---|
future | 结果接收端 | 不负责执行 | 不负责写 | 自己 get() | 等待异步结果、传异常 | 接口统一,支持等待与异常传播 | 一般只能读一次,功能偏基础 |
promise | 结果生产端 | 不负责执行 | 手动 set_value/set_exception | 关联的 future | 手动控制完成时机、事件驱动回传结果 | 灵活,边界清晰 | 容易遗漏异常路径或 broken promise |
packaged_task | 任务包装器 | 调用它的人/线程 | 执行完成后自动写 | 关联的 future | 任务队列、线程池、自定义调度 | 把“执行”和“回传结果”绑定,适合任务化模型 | 接口比 async 更底层,管理更繁琐 |
async | 高层异步启动接口 | 标准库根据策略处理 | 内部自动写 | 返回的 future | 简单异步任务 | 代码最短,异常传播天然支持 | 策略不显式时行为不透明,不是线程池接口 |
std::thread 与 std::async 对比
| 维度 | std::thread | std::async |
|---|---|---|
| 关注点 | 启动执行体 | 启动任务并返回结果句柄 |
| 返回值处理 | 需要手动传回 | 直接拿 future |
| 异常传播 | 需自己处理 | 自动通过 future::get() 传播 |
| 资源管理 | 需 join/detach | 由 future/shared state 参与管理 |
| 适用场景 | 强控制线程生命周期 | 更关注“异步算完把结果给我” |
| 常见误区 | 只开线程不管结果 | 误以为一定建新线程或等于线程池 |
future 与 shared_future 对比
| 维度 | future | shared_future |
|---|---|---|
| 读取次数 | 一般一次 | 可多次读取 |
| 所有权 | 独占 | 共享 |
| 是否可拷贝 | 不可拷贝,通常 move-only | 可拷贝 |
| 场景 | 单消费者 | 多消费者 |
std::launch::async 与 std::launch::deferred 对比
| 策略 | 何时执行 | 由谁执行 | 特点 | 适用场景 |
|---|---|---|---|---|
async | 尽快异步执行 | 通常在独立执行上下文 | 真正并发的可能性更高 | 明确需要异步并行 |
deferred | get()/wait() 时才执行 | 调用等待函数的线程 | 惰性执行,不一定并发 | 可能不需要执行、或希望延后成本 |
易错点
- 把
future当成线程本身。future只是结果句柄,不代表线程。 - 认为
promise会自动启动任务。 不会,它只负责写结果。 - 不知道
future::get()会“消费结果”。 想多次读取要用shared_future。 - 用
std::async却不提启动策略。 这是面试里经常被继续追问的点。 - 以为
std::async一定创建新线程。 标准不保证,也不等于线程池。 - 忽略异常传播。
future不仅能传值,也能把异常重新抛给调用者。 - 忘了处理
promise未写结果就析构的情况。 这会导致 broken promise。 - 线程示例里随手按引用捕获
promise,却没考虑生命周期。 更稳妥的是把promisemove 到线程里。 - 丢弃
async的返回future。 可能隐藏同步/阻塞语义,工程里不建议。
记忆技巧
-
一条通信链:
promise写结果future读结果packaged_task执行后自动写async帮你把执行和回传都组起来
-
一句高频记忆句:
thread解决“谁去做”,future/promise解决“结果怎么回来”。 -
场景记忆法:
- 手动回传:
promise - 任务对象化:
packaged_task - 最省事直接用:
async - 多处读结果:
shared_future
- 手动回传:
-
判断题口诀:
要不要自己控制完成时机?要,就想
promise。 要不要把函数塞进任务队列?要,就想packaged_task。 只是想异步算完拿结果?优先async。
面试速答版
future、promise、packaged_task、async 都是为了解决异步结果传递问题。future 是读端,用来等待并获取结果;promise 是写端,手动设置返回值或异常;packaged_task 是把一个可调用对象包装成任务,执行后会自动把结果写进关联的共享状态;async 是更高层的封装,负责启动任务并直接返回 future。它们底层都有共享状态,所以不仅能传值,也能传异常。工程里简单异步任务优先用 async,要手动控制结果回传就用 promise,做任务队列或线程池更适合 packaged_task。另外 future::get() 一般只能调用一次,如果多个地方要读同一个结果,就用 shared_future。
面试加分版
这套工具我一般分成两层理解:执行层和结果层。thread 或 async 更偏执行层,决定任务怎么跑;future/promise 更偏结果层,决定结果、异常、完成状态怎么传回来。
先说关系:future 是结果接收端,promise 是结果发送端,它们通过一个共享状态关联起来。共享状态里不只是返回值,还包含异常和 ready 标记,所以 future::get() 既能拿值,也能把异步任务里的异常重新抛到调用方。promise 适合手动控制完成时机,比如外部事件到了、某个 worker 真正结束了,再 set_value 或 set_exception。
packaged_task 可以理解为“带 future 出口的任务包装器”。你给它一个函数,任务执行完成后会自动把返回值写入共享状态,所以它特别适合任务队列、线程池这种场景,因为你既能把任务对象塞进队列,又能把 future 返回给调用方。
async 则是最高层封装,既帮你创建共享状态,也帮你启动任务并返回 future,所以写法最简单。但面试里我一定会强调 launch policy:std::launch::async 表示异步执行,std::launch::deferred 表示延迟到 get()/wait() 时才执行。默认策略可能让实现自己决定,所以工程里如果我关心时机和并发行为,通常会显式写 std::launch::async。
最后说选择原则:如果只是“异步执行一下然后拿结果”,优先 async;如果执行和完成时机是分离的,比如事件驱动或手动通知,就用 promise;如果要做任务调度、线程池、任务队列,就更适合 packaged_task。另外要注意 future::get() 一般只能调用一次,多个消费者要共享结果时应该改用 shared_future。