C++11/17 不支持原生链式 future,必须用 std::promise 手动封装回调;禁止捕获不可拷贝的 std::future,应捕获 std::shared_ptr 并在完成时调用 set_value() 或 set_exception()。

如何用 std::future 和 std::promise 构建可链式调用的异步任务
直接说结论:C++11 之后的标准库不支持原生的「链式 future」(比如 .then()),必须手动封装;靠裸用 std::future::get() 会阻塞,彻底破坏异步流。核心解法是把后续任务包装成回调,由前序任务完成时触发调度。
典型错误是写成这样:
auto f1 = std::async([]{ return 42; });
auto f2 = std::async([f1]{ return f1.get() * 2; }); // 错!f1.get() 在子线程里阻塞,且 f1 被拷贝(非法)
- 必须用
std::promise手动控制状态流转,让前序任务在完成时调用set_value()或set_exception() - 后续任务不能捕获
std::future对象(不可拷贝、仅能 move),应捕获其对应的std::shared_ptr<:promise></:promise> - 调度器建议用
std::thread+ 队列,或直接复用std::async的线程池(但注意默认策略是std::launch::async | std::launch::deferred,可能延迟执行)
为什么不能直接对 std::future 调用 then()?C++20 前的现实约束
C++20 引入了 std::future::then(),但 GCC 12/Clang 14 之前默认不启用,且要求编译器开启实验性协程支持(-fcoroutines + -std=c++20)。绝大多数生产环境仍跑在 C++17,所以得自己造轮子。
常见误判是以为 std::async 返回的 std::future 天然支持组合——它只提供等待和取值能力,不提供注册回调的接口。强行用 wait_for(1ms) 轮询检查状态,既耗 CPU 又不准。
- 标准规定
std::future是「一次性消费」对象:调用get()或wait()后,再次调用会抛std::future_error(错误码std::future_errc::no_state) - 若需多次监听,必须用
std::shared_future,但它仍不提供回调机制,只是允许多个消费者 - 真正可行的路径是:用
std::promise暴露get_future(),再在线程中调用set_value(),由外部统一管理回调队列
一个轻量级链式异步任务类的关键实现点
下面这个结构体不是玩具,而是生产可用的最小闭环:
template<typename t>
struct Task {
std::shared_ptr<:promise>> prom;
std::function<void> work;
<pre class="brush:php;toolbar:false;">Task(std::function<void> f) : work(std::move(f)) {
prom = std::make_shared<:promise>>();
}
template<typename u>
Task<u> then(std::function<u> next) {
auto next_task = std::make_shared<:promise>>();
auto self = this->prom;
this->work = [self, next_task, next, work = std::move(this->work)]() mutable {
try {
work(); // 执行当前任务
auto val = self->get_future().get(); // 安全:只在此处 get,且仅一次
next_task->set_value(next(val));
} catch(...) {
next_task->set_exception(std::current_exception());
}
};
return Task<u>{[next_task](){ /* 空占位,实际由下游调用 run() 触发 */ }};
}
void run() { std::thread([this]{ work(); }).detach(); }</u></:promise></u></u></typename></:promise></void>
};
- 每个
Task持有独立的std::promise,避免状态污染 -
then()不立即执行,只组装逻辑;run()才真正派生线程——这保证了链式声明和执行分离 - 异常必须用
std::current_exception()捕获并透传,否则下游get()会卡死或崩溃 - 注意
std::thread::detach()有风险:若任务引用了栈变量,可能悬挂。更稳妥的做法是用线程池或std::jthread(C++20)
实战中容易被忽略的三个坑
写完能跑通不代表没隐患。这些点在压测或长时间运行时才暴露:
- 移动语义陷阱:
std::promise和std::future都不可拷贝,所有 lambda 捕获必须用=或显式[prom = std::move(prom)],否则编译失败或静默 UB - 线程生命周期失控:如果主线程结束太快,而异步任务还在跑,
std::thread析构会 terminate。务必确保所有std::thread被join()或用std::jthread - 内存泄漏风险:若任务链中间某环抛异常且未被捕获,
std::promise对象可能永远不被 set,导致后续所有依赖它的任务永久挂起——必须给关键任务加超时和 fallback 逻辑
链式异步真正的复杂点不在语法,而在状态流转的确定性和资源归属的清晰性。别急着堆模板,先用最简 std::promise + std::thread 跑通一条链,再逐层加固。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











