在多线程编程中,一个高频需求是:把任务丢给另一个线程执行,然后在合适的时机取回结果。传统做法是用全局变量加锁加条件变量,代码繁琐且容易出错。C++11在标准库中引入了std::future和std::promise这对工具,把“异步结果的传递”抽象成了类型安全的组件,从此获取异步结果变成了一件声明式的事情。本文将系统讲解它们的工作原理、典型用法和常见陷阱。

一、核心原理:共享状态是_future和promise的桥梁
std::future和std::promise本质上是对同一块“共享状态(shared state)”的两个句柄。可以把它想象成一根管道的两端:promise是写入端,负责在某处存放结果或异常;future是读取端,负责在另一处取出结果。一个共享状态最多只能关联一个promise实例和一个future实例,这保证了结果传递的唯一性。
这块共享状态通常由std::promise在构造时创建(堆上分配,内部通过引用计数管理生命周期)。当调用promise::set_value时,结果被写入共享状态并置为“就绪”;当调用future::get时,读取端会阻塞直到状态就绪,然后取出值并使future失效。整个过程由标准库内部加锁保护,使用者完全不需要手写同步代码。
一个容易被忽略的细节是异常传递。如果在异步任务中抛出了异常,可以用promise::set_exception把它存入共享状态,读取端调用get时异常会被重新抛出。这意味着异步代码的错误处理可以和同步代码保持同样的书写风格:
#include <future>
#include <iostream>
#include <stdexcept>
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread producer([&prom]() {
try {
throw std::runtime_error("任务执行失败");
} catch (...) {
// 把当前异常存入promise,读取端get时会重新抛出
prom.set_exception(std::current_exception());
}
});
try {
int result = fut.get(); // 这里会重新抛出runtime_error
} catch (const std::exception& e) {
std::cerr << "捕获到异步异常: " << e.what() << std::endl;
}
producer.join();
return 0;
}二、获取future的几种方式:async、packaged_task与promise
直接使用future和promise是最底层的方式,标准库还提供了两个更高层的封装。第一种是std::async,它接收一个可调用对象和参数,返回一个future,函数会在新线程(或调用方线程,取决于启动策略)中执行,返回值自动写入共享状态:
#include <future>
#include <numeric>
int slow_sum(int a, int b) {
// 模拟耗时计算
return a + b;
}
int main() {
// std::launch::async强制在新线程执行
std::future<int> fut = std::async(std::launch::async, slow_sum, 10, 32);
// 主线程可以先做别的事情
int result = fut.get(); // 阻塞等待,得到42
return 0;
}第二种是std::packaged_task,它把一个可调用对象包装起来,使其调用结果自动关联到future。packaged_task本身不会开线程,需要你把它扔进线程池或线程中手动执行,这让它成为构建线程池的核心组件。三者的关系可以这样理解:promise要求你手动调用set_value;packaged_task自动帮你把函数返回值送进promise;async则更进一步,连线程都帮你开好了。
需要特别警惕std::async的析构行为:由std::launch::async启动的任务,其future析构函数会阻塞等待任务完成。如果临时创建的future没有被保存,任务实际上会退化成同步执行,这是许多“异步代码不异步”问题的根源。因此务必用变量接住async的返回值。
三、get与wait的区别以及future的move语义
future::get是“取出”语义:它会阻塞直到结果就绪,然后把值移动出来(对于非引用类型),调用后future变为无效状态,只能调用一次。如果需要多次等待或在多个地方感知结果,应该使用std::shared_future,它允许拷贝且支持多次get。
future::wait则是“等待”语义:只阻塞到就绪,不取值、不失效。它还有两个带超时的版本:wait_for接受一个时间段,返回std::future_status枚举(ready、timeout或deferred),wait_until接受一个绝对时间点。这些接口是实现轮询、超时取消等高级模式的基础:
std::future<int> fut = std::async(std::launch::async, slow_sum, 1, 2);
// 最多等待100毫秒
if (fut.wait_for(std::chrono::milliseconds(100)) == std::future_status::ready) {
int v = fut.get();
} else {
std::cerr << "任务超时,尚未完成" << std::endl;
}另一个重要特性是future只能移动、不能拷贝。这不是缺陷而是设计:如果多个future都能get同一个结果,值到底给谁?移动语义强制你在代码中显式转移“读取权”,比如把future作为参数传给另一个函数或存入容器,所有权一目了然。C++14起还提供了std::future::share()方法,可以把一个future转换为shared_future分发给多个等待者。
四、实战与陷阱:在线程池中使用future
future最常见的生产级应用是给线程池提供“提交任务返回结果”的能力。实现思路是用packaged_task包装任意任务,由于packaged_task只能移动而std::function要求可拷贝,通常需要再做一层类型擦除,例如用std::function<void()>包装移动后的packaged_task:
template <typename F>
auto submit(F&& f) -> std::future<decltype(f())> {
// 注意:packaged_task只能移动,需用shared_ptr包一层再放入function
auto task = std::make_shared<std::packaged_task<decltype(f())()>>(
std::forward<F>(f));
std::future<decltype(f())> res = task->get_future();
// task_queue_.push([task]() { (*task)(); });
return res;
}最后梳理几个典型陷阱。第一,promise只能set_value一次,重复设置会抛出std::future_error,如果任务可能被取消,可以用set_exception或空指针约定代替。第二,注意悬垂引用:lambda按引用捕获局部变量,而future的get在线程外延后执行,局部变量可能早已销毁,捕获列表应尽量按值或配合std::move。第三,避免双向等待死锁:线程A等线程B的结果,线程B又在等A持有的资源,这类问题在复杂异步链路中很隐蔽,设计时应保持单向依赖。
总结来说,std::future和std::promise把异步结果的传递从手工同步解放为类型安全的抽象。掌握共享状态模型、理解get的一次性语义、牢记async的析构阻塞特性,就能在日常开发中稳定地驾驭C++异步编程。如果需要更强大的组合能力,可以进一步研究C++20的协程以及第三方库的continuation式future实现。
std::futurestd::promise异步编程修改时间:2026-09-01 12:06:54