std::promise是C++11异步模型中的生产者端点,与std::future共享状态,实现线程间一次性结果传递。它通过
set_value或set_exception写入结果或异常,get_future()获取消费者视图,get()阻塞等待结果。对象不可拷贝,只能移动,确保唯一所有权。适用于单次结果返回、跨线程异常传播及事件信号,但不支持多生产/消费或频繁通信。其底层机制被std::async和std::packaged_task封装,是手动控制异步结果的理想选择。
一、状态机模型
Producer Thread Shared State Consumer Thread
────────────── ───────────── ──────────────
std::promise 值 / 异常 std::future
set_value / set_exception ───▶ ready = true ───▶ get / wait
核心性质:
-
promise/future 是一对一次性通信通道
-
共享状态可以持有:
-
一个
T类型的值 -
一个异常(
std::exception_ptr) -
或者最终变成
broken_promise
-
-
set_value/set_exception会使共享状态变为 ready -
future::get()阻塞直到共享状态 ready,并取走结果或重抛异常
二、对象不可拷贝,只能移动
std::promise<int> p;
std::promise<int> q = p; // 编译错误:拷贝构造被删除
std::promise<int> r = std::move(p); // OK,p 此后无共享状态
规则:
-
拷贝构造 / 拷贝赋值:被删除
-
移动构造 / 移动赋值:转移共享状态所有权
-
一个 promise 只能
get_future()一次 -
一个共享状态只能由一个 promise 写入
三、核心成员函数
3.1 get_future()
std::promise<int> p;
std::future<int> f = p.get_future();
语义:
-
返回与当前 promise 共享同一共享状态的
future -
第二次调用抛出
std::future_error,错误码future_already_retrieved -
promise 被 move 走后,原对象无共享状态,调用会抛
no_state
顺序要求 :通常先 get_future(),再把 promise move 给工作线程。
3.2 set_value
p.set_value(42);
语义:
-
将值写入共享状态
-
原子地使共享状态变为 ready
-
唤醒所有在
future::get/wait上阻塞的线程
约束:
-
只能调用一次
-
再次调用抛
std::future_error,错误码promise_already_satisfied -
若 promise 无共享状态,抛
no_state -
值类型会按值/引用/右值规则拷贝或移动构造
引用特化:
std::promise<const std::string&> p;
std::string s = "data";
p.set_value(s); // 存的是引用,调用方必须保证生命周期
void特化:
std::promise<void> p;
p.set_value(); // 仅作"事件已发生"的信号
3.3 set_exception
try {
do_work();
p.set_value(result);
} catch (...) {
p.set_exception(std::current_exception());
}
语义:
-
将
std::exception_ptr存入共享状态 -
共享状态变为 ready
-
消费者调用
future.get()时,异常会在消费者线程被重新抛出
这是 C++ 线程间安全传递异常的标准方式。不用 promise 时,工作线程未捕获异常会直接 std::terminate。
3.4 set_value_at_thread_exit
p.set_value_at_thread_exit(42);
语义:
-
立即把值放进共享状态,但不马上置为 ready
-
当前线程退出、所有线程局部对象析构完成后,才把共享状态标记为 ready
-
消费者会一直阻塞到生产者线程结束
适用场景:
-
结果依赖线程局部存储(TLS)
-
希望"线程真正收尾后再让别的线程看到结果"
-
避免消费者拿到结果后,生产者线程的 TLS 还没析构造成逻辑错误
对应异常版本:
p.set_exception_at_thread_exit(std::current_exception());
四、析构与 broken_promise
如果 promise 被销毁,而共享状态仍然没有被 set:
-
标准规定:promise 放弃共享状态时,会向共享状态写入
std::future_error(std::future_errc::broken_promise) -
共享状态变为 ready
-
消费者
future.get()会抛出该异常,而不是永久阻塞std::future
f;
{
std::promisep;
f = p.get_future();
// p 析构,但未 set_value
}
f.get(); // 抛 std::future_error(broken_promise)
工程含义:
promise 的所有权必须交给"真正会兑现结果"的执行流。
错误示例:
void worker(std::promise<int>& p) {
// 如果某些分支没 set_value,p 析构时 broken_promise
}
更好:
void worker(std::promise<int> p) {
try {
p.set_value(compute());
} catch (...) {
p.set_exception(std::current_exception());
}
}
五、线程安全性
标准保证:
-
对同一个共享状态的内部读写是同步的
-
set_value与future::get之间存在 happens-before 关系 -
set_value、set_exception、set_value_at_thread_exit、set_exception_at_thread_exit在更新 promise 对象时,表现得像获取了与该 promise 关联的同一个互斥体
但是:
多个线程同时调用同一个 promise 对象 的
set_value是未定义/非法用法。
正确模型:
-
一个 promise:单一生产者
-
一个 future:单一消费者
-
多消费者请用
std::shared_futurestd::promise
p;
std::shared_futuresf = p.get_future().share(); std::thread a([sf]{ std::cout << sf.get(); });
std::thread b([sf]{ std::cout << sf.get(); });
六、与 std::async/ std::packaged_task的关系
三者底层都是 promise/future:
| 工具 | 谁创建 promise | 适用场景 |
|---|---|---|
std::promise |
用户手动 | 非函数式、事件驱动、手动通知 |
std::packaged_task |
包装可调用对象 | 把"函数对象"当任务投递给线程池 |
std::async |
标准库内部 | 一次性异步函数调用 |
std::packaged_task本质:
struct packaged_task {
promise<R> p;
F f;
void operator()(Args... args) {
try {
p.set_value(f(args...));
} catch (...) {
p.set_exception(std::current_exception());
}
}
};
七、典型用法
7.1 线程返回结果
void worker(std::promise<int> p) {
p.set_value(heavy_compute());
}
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread t(worker, std::move(p));
int result = f.get();
t.join();
7.2 跨线程异常传播
void worker(std::promise<double> p) {
try {
double v = risky();
p.set_value(v);
} catch (...) {
p.set_exception(std::current_exception());
}
}
auto f = p.get_future();
try {
double v = f.get();
} catch (const std::runtime_error& e) {
// 处理工作线程抛出的异常
}
7.3 用 void promise 做一次性信号
std::promise<void> ready;
std::future<void> f = ready.get_future();
std::thread producer([&ready] {
prepare_resources();
ready.set_value(); // 发信号:资源准备好了
});
f.wait(); // 消费者等资源就绪
start_consuming();
这比 mutex + condition_variable简洁,但只能通知一次。
八、常见错误
8.1 多次 set
p.set_value(1);
p.set_value(2); // 抛 promise_already_satisfied
8.2 promise 生命周期短于工作线程
std::future<int> f;
std::thread t;
{
std::promise<int> p;
f = p.get_future();
t = std::thread([](std::promise<int> p){
p.set_value(1);
}, std::move(p));
}
t.join();
上面这段其实没问题,因为 promise 已经 move 进线程了。
真正危险的是:
std::promise<int> p;
auto f = p.get_future();
std::thread([&p]{ p.set_value(1); }).detach();
// 函数返回,p 可能已析构,线程还在跑 → UB / broken_promise
规则:
promise 要么 move 进线程,要么生命周期由智能指针/容器管理。
8.3 把 promise 当队列用
promise/future 是一次性的:
-
不适合多次生产/消费
-
不适合事件流
-
不适合管道消息
需要这些能力时,用:
-
std::queue + mutex + condition_variable -
无锁队列
-
线程池任务队列
九、适用边界
适合:
-
一个任务产出一个结果
-
工作线程不是简单函数调用,而是事件/回调/状态机
-
需要手动控制"什么时候兑现结果"
-
需要跨线程传异常
不适合:
-
高频小任务(用线程池)
-
多生产者多消费者消息流(用队列)
-
多次通知(用 condition_variable / channel)
-
简单异步函数调用(用
std::async)
十、一句话总结
std::promise是"我以后会给你一个结果"的契约对象;
set_value/set_exception是履约;
future.get()是等待履约;不履约,就是
broken_promise。
它是 C++ 并发模型里比 std::thread抽象、比 std::async底层的那一层:
当你需要"手动兑现结果"时,用它;当你只想要"函数异步返回值"时,别直接用它。