在现代C++编程中,std::future和std::promise是异步编程模型中的两个重要组件,它们构成了C++标准库中处理异步计算结果的基础。本文将深入浅出地介绍这两个概念,探讨它们的应用场景、常见问题、易错点及如何避免,同时辅以代码示例,帮助读者更好地理解和运用这些机制。
- 5 分钟读完,会写、会读、不踩坑。
- 完整版(机制细节、超时与取消设计、RPC 实战案例)参见猫哥的另一篇文章C++ 异步编程:std::future 与 std::promise 详解。
1. 它解决什么问题
线程之间要传一个结果,而且这个结果很晚才产生。你不想用全局变量加轮询,也不想为"等结果"手写条件变量。
std::promise / std::future 就是标准库给的答案:一个结果,两个把手。
text
std::promise<int> std::future<int>
(写端) (读端)
| |
| 共享状态(堆上,引用计数) |
+------> [值 42 | 就绪标志] <------+
一次性、不可逆
- 写端 用
set_value()把结果放进去 ------ 只能放一次。 - 读端 用
get()把结果取出来 ------ 只能取一次;没就绪就一直等。
一句话:写端定终局一次,读端取结果一次。 后面所有"只能用一次"的限制,都是这句话推出来的。
2. 最小可用例子
cpp
#include <chrono>
#include <future>
#include <iostream>
#include <thread>
int main()
{
std::promise<int> p; // 写端
std::future<int> f = p.get_future(); // 读端(这一步不阻塞!)
std::thread t([&p] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
p.set_value(42); // 干活干完了,交结果
});
std::cout << "waiting for result...\n";
std::cout << "result = " << f.get() << "\n"; // 阻塞直到就绪,取走结果
t.join();
}
输出(实测):
text
waiting for result...
result = 42
f.get() 之前的 cout 一定先打印 ------ 因为 get() 之前没有任何阻塞。
3. 三条必须记住的规则
| 规则 | 说明 |
|---|---|
get_future() 不等待 |
它只是取回读端把手,立刻返回 |
set_value() 只能一次 |
第二次抛 promise_already_satisfied |
get() 通常只能一次 |
取走后 future 就失效了,再 get() 抛 no_state |
第二条和第三条犯错时抛的都是 std::future_error,可以直接抓:
cpp
try { p.set_value(2); } // 第二次设置
catch (const std::future_error& e) {
// e.code() == std::future_errc::promise_already_satisfied
}
判断错误类型时请比较枚举值 (
e.code() == std::future_errc::no_state), 不要比较e.code().value()的数字 ------ 那是实现相关的。
4. 更省事的写法:std::async
不想手动管理 promise 和线程时,让 std::async 替你干:
cpp
#include <future>
#include <iostream>
int compute() { return 6 * 7; }
int main()
{
auto f = std::async(std::launch::async, compute); // 立刻起线程
std::cout << "result = " << f.get() << "\n"; // 42
}
但有一个坑必须知道 :std::async 返回的 future,如果没被接住,析构时会阻塞到任务结束。
cpp
std::async(std::launch::async, slow_job); // 返回值被丢弃 → 这行会等 slow_job 跑完
所以这里有个反直觉的结论:
std::async不能用来"扔出去就不管"(即发即忘)------漏接返回值等于同步执行。std::async也当不了线程池 ------ 每次调用都可能新建线程。
另外,std::async 的默认策略是 async | deferred,实现有权选择推迟到 get() 时才执行 。想要真异步,就显式写 std::launch::async。
5. 异常怎么跨线程传
写端抛了异常怎么办?promise 不会自动捕获 ------ 但你可以把它"存进去",读端 get() 时会重新抛出来:
cpp
#include <exception>
#include <future>
#include <iostream>
#include <stdexcept>
#include <thread>
int main()
{
std::promise<int> p;
auto f = p.get_future();
std::thread t([&p] {
try {
throw std::runtime_error("sensor offline");
} catch (...) {
p.set_exception(std::current_exception()); // 把异常交给读端
}
});
try {
std::cout << f.get() << "\n";
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << "\n"; // caught: sensor offline
}
t.join();
}
这就是它优于手工同步的地方:线程边界上的异常能被正常地当作异常处理,而不是被吞掉或者让程序崩掉。
注意:如果写端什么都没设就析构了 (比如提前 return、抛了异常没接住),读端
get()会抛std::future_error,错误码是broken_promise。这是标准库在帮你把"永久阻塞"变成"可捕获的异常"。
6. 等一会儿:wait_for / wait_until
不想无限等,就加超时:
cpp
#include <chrono>
#include <future>
#include <iostream>
#include <thread>
int main()
{
std::promise<int> p;
auto f = p.get_future();
std::thread t([&p] {
std::this_thread::sleep_for(std::chrono::milliseconds(300));
p.set_value(1);
});
using namespace std::chrono_literals;
if (f.wait_for(100ms) == std::future_status::ready)
std::cout << "value = " << f.get() << "\n";
else
std::cout << "timeout, 但我只等到这里\n"; // 走这条
t.join(); // 任务还在跑,正常退出前得等它
}
关键认知:超时 ≠ 取消。
wait_for 返回 timeout 只表示"我不等了",后台那个任务照样在跑 ,还会把结果写进共享状态。标准库没有提供取消 std::future 的 API。
所以超时之后,你必须自己决定:表项谁清理、迟到的结果怎么办。(这是入门之后最容易出错的地方,完整版第 8、9 章专门讲。)
三个返回值:
wait_for 返回 |
含义 |
|---|---|
future_status::ready |
结果已就绪,可以 get() 了 |
future_status::timeout |
到点了还没好,任务还在跑 |
future_status::deferred |
任务是推迟执行的,得 get() 或 wait() 才会跑 |
7. 一个结果给多个人:shared_future
std::future 的 get() 只能调一次。多个消费者要读同一个结果,用 std::shared_future:
cpp
#include <future>
#include <iostream>
#include <thread>
int main()
{
std::promise<int> p;
std::shared_future<int> sf = p.get_future().share(); // 注意是 share()
std::thread t([&p] { p.set_value(42); });
std::thread a([sf] { std::cout << "A: " << sf.get() << "\n"; });
std::thread b([sf] { std::cout << "B: " << sf.get() << "\n"; });
t.join(); a.join(); b.join();
}
sf.get() 可以反复调用(线程安全,返回值拷贝),两个线程都拿到 42。注意捕获的是 sf 的拷贝 ------ 每个线程各有自己的 shared_future 副本,共享底层状态。
8. 追问:std::async 能替代 promise 吗?
只能替代一部分,不能整体替代。 分界线是一句话:
结果是不是"任务函数返回时"自然产生的?是 →
std::async能替代,而且更省事。不是 → 替代不了。
本质差别:写端在谁手上
promise 的核心能力不是"设置结果",而是写端可以被移交:在 A 线程创建它,塞进一张请求表,最后由 C 线程(IO 线程、回调线程、超时扫描线程)来填结果。
而 std::async 的写端是它内部造的,你拿不到 ------ 结果只能由那个函数 return 或 throw 产生。
这个差别在"超时"场景下会变成实打实的耗时差距(任务都是 500ms,超时都设 100ms):
cpp
#include <chrono>
#include <exception>
#include <future>
#include <iostream>
#include <stdexcept>
#include <thread>
using Clock = std::chrono::steady_clock;
using namespace std::chrono_literals;
int main()
{
// A:promise ------ 超时后能主动叫醒等待方(写端在手上)
{
std::promise<int> p;
auto f = p.get_future();
std::thread worker([&p] {
std::this_thread::sleep_for(500ms);
try { p.set_value(1); }
catch (const std::future_error&) { /* 已被超时路径先手关闭,正常 */ }
});
const auto t0 = Clock::now();
if (f.wait_for(100ms) == std::future_status::timeout) {
// 关键:把"超时"这个结果投递进去
p.set_exception(std::make_exception_ptr(std::runtime_error("timed out")));
try { (void)f.get(); } catch (const std::exception&) {}
}
std::cout << "A(promise): "
<< std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count()
<< " ms\n";
worker.join(); // 后台线程仍在收尾(不计入上面的数字)
}
// B:async ------ 没有写端,只能干等任务跑完
{
auto f = std::async(std::launch::async, [] {
std::this_thread::sleep_for(500ms);
return 1;
});
const auto t0 = Clock::now();
if (f.wait_for(100ms) == std::future_status::timeout) {
(void)f.get(); // 没有任何 API 能让它立刻就绪
}
std::cout << "B(async) : "
<< std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count()
<< " ms\n";
}
}
输出(实测):
text
A(promise): 100 ms
B(async) : 500 ms
A 之所以是 100ms,是因为写端在手上 ------ set_exception() 把"超时"当作结果投递了进去。B 连写端都没有,"超时"这件事根本无法表达给等待方,只能眼睁睁等任务跑完。
能力对照
| 能力 | promise + future |
std::async |
|---|---|---|
| 结果由谁产生 | 任意线程、任意时刻 | 只由任务函数 return/throw |
| 写端能否移交 | ✅ 核心能力 | ❌ 拿不到 |
| 超时后立刻唤醒等待者 | ✅ 实测 100 ms | ❌ 实测 500 ms |
| 任务句柄(取消/转移/查询) | 自己管 | ❌ 没有 |
| 线程模型 | 自己定,可复用线程池 | 每次调用可能新建线程 |
| 临时 future 析构 | 不阻塞 | ⚠️ 阻塞到任务结束 |
| 默认策略 | --- | ⚠️ 可能是 deferred |
能替代的部分
"后台算个返回值、我要等它"这一类,是纯粹的简化,直接换:
cpp
// 旧:promise + 手工线程,只为算个值
std::promise<int> p;
auto f = p.get_future();
std::thread t([&p] { p.set_value(compute()); });
const int v = f.get();
t.join();
// 新:语义等价,短得多
auto f = std::async(std::launch::async, compute);
const int v = f.get();
两个前提别漏:显式写 std::launch::async (默认允许推迟执行),返回值必须被接住(临时 future 析构会阻塞)。
替代不了的部分
| 场景 | 为什么不行 |
|---|---|
| RPC 客户端收响应 | 填结果的是 IO 线程,必须有可移交的写端 |
| 超时后主动关闭共享状态 | 没有写端 → 只能等到任务结束,超时形同虚设 |
| 线程池 / 限流 | std::async 每次可能新建线程,不是池 |
| 一个结果给多方读 | 得先 .share(),而 async 没有可共享的登记点 |
| 需要真正取消任务 | 没有任务句柄,"取消"没地方表达 |
想"两全":packaged_task
既要 std::async 的"自动填结果",又要 promise 的"写端可控",用 std::packaged_task:
cpp
std::packaged_task<int()> task(compute);
auto f = task.get_future(); // 读端可以像 promise 一样发出去
pool.submit(std::move(task)); // 但任务跑在哪儿由你决定
const int v = f.get();
决策规则
- 结果 = 函数返回值,且你愿意等它跑完 →
std::async(std::launch::async, f),记得接住 future。 - 结果由别的地方、别的时刻产生 (RPC、回调、超时、事件) →
promise+future。 - 想两者兼得 (自动填结果 + 任务投给自己的池子) →
packaged_task+ 线程池。
9. 速查与建议
基本流程
text
promise.get_future() → 把 future 交给消费者
promise.set_value(v) / set_exception(e) → 定终局(一次)
future.get() → 阻塞取值(一次,会取走)
用哪个?
| 场景 | 用什么 |
|---|---|
| 一个异步操作的结果 | std::promise + std::future |
| 结果要给多方读 | std::shared_future(.share()) |
| 就想异步算个值、懒得管线程 | std::async(std::launch::async, f),记得接住返回值 |
| 有返回值的一批任务 | packaged_task + 线程池 |
| 真要能中断任务 | C++20 std::jthread + std::stop_token |
| 只等完成、不要值 | std::latch / std::call_once |
四条容易踩的坑
- 漏接
std::async的返回值 ------ 那行变成同步等待。 - 忘记
std::async默认允许deferred------ 显式写launch::async。 - 以为
wait_for超时能取消任务 ------ 它不能,超时后你得自己收尾。 - 没人
get()的 future ------ 里面的异常会被安静地丢掉,程序不崩,你也永远不知道出了问题。
最后一句 :future / promise 本身很简单,难的是围绕它的纪律 ------ 谁登记、谁清理、谁负责关闭。这四条坑里有三条都属于"纪律"问题,而不是 API 用法问题。