异步任务与期物:async, future 与 promise (Asynchronous Programming)¶
在掌握了 std::thread、互斥锁与条件变量之后,很多初学者在编写实际业务时常常感到繁琐痛苦:
“我只是想在后台开一个子线程去计算斐波那契数列,或者异步读取一个大文件,拿到返回值而已。难道为了拿回一个简单的
int,我非要声明一个全局共享变量、定义一把互斥锁、还要再写一个条件变量去通知主线程吗?” “如果子线程在运行过程中抛出了异常,主线程怎么能捕获到这个异常?”
C++11 在标准库 <future> 中,为我们提供了一套更现代、更高级的基于任务(Task-Based)的异步编程抽象。它把复杂的线程创建、锁管理和生命周期调度封装在幕后,让开发者只需关注“提交任务”与“获取结果”。
本文将为你通俗拆解这套由 std::future、std::promise、std::packaged_task 与 std::async 构成的异步编程体系。
1. 概念模型:提货单与单向通道¶
要理解异步编程,最形象的类比就是网购或者在餐厅点餐:
sequenceDiagram
autonumber
actor Customer as 主线程 (顾客)
participant Kitchen as 后台子线程 (后厨)
Customer->>Kitchen: 1. 点餐: 发起耗时计算任务
Kitchen-->>Customer: 2. 立即返回一张【提货单】 (std::future)
Note over Customer: 顾客拿着提货单,可以自由去刷手机、干别的事
Note over Kitchen: 后厨在后台热火朝天地做菜 (执行耗时任务)
Customer->>Customer: 3. 准备吃饭,来到前台出示提货单: fut.get()
Note over Customer: 如果菜还没做好,顾客在柜台短暂等待 (阻塞)
Kitchen-->>Customer: 4. 出餐完毕,取走香喷喷的菜品 (拿回真实计算结果或捕获异常)
std::future(期物 / 提货凭证):代表一个“在未来某个时刻才能拿到的结果”。它是数据的读取端;std::promise(承诺 / 交付端):代表履约的责任方。它是数据的写入端。
两者就像一个跨线程的单向通信管道(Channel),无论子线程是正常算出了结果,还是中途遭遇崩溃抛出了异常,都能通过这个管道安全无损地传递给主线程!
2. 深入理解 std::promise 与 std::future¶
std::promise 和 std::future 通常成对出现:
#include <iostream>
#include <thread>
#include <future>
#include <stdexcept>
// 子线程工作函数: 接收一个 promise 的写端
void async_worker(std::promise<int> prom, int x) {
try {
if (x < 0) {
throw std::invalid_argument("参数不能为负数!");
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
prom.set_value(x * x); // 成功履约: 写入结果
} catch (...) {
// 捕获异常,并原封不动地通过 promise 传递出去!
prom.set_exception(std::current_exception());
}
}
int main() {
// 1. 创建 promise 对象 (写入端)
std::promise<int> prom;
// 2. 从 promise 中获取绑定的 future 凭证 (读取端)
std::future<int> fut = prom.get_future();
// 3. 将 promise 移动语义 (std::move) 传入子线程 (promise 不可拷贝)
std::thread t(async_worker, std::move(prom), -5);
std::cout << "主线程正在并行做其他事情...\n";
try {
// 4. 调用 get() 阻塞等待并取回结果
// 如果子线程抛出了异常,get() 会在主线程重新抛出该异常!
int result = fut.get();
std::cout << "异步计算结果: " << result << "\n";
} catch (const std::exception& e) {
std::cout << "主线程成功捕获到子线程的异常: " << e.what() << "\n";
}
t.join();
return 0;
}
核心特性警示:¶
- 只能读取一次:
fut.get()拥有移动语义,一旦调用,该future的状态就会失效,再次调用会抛出std::future_error异常; - 多线程广播(
std::shared_future):如果需要 5 个工作线程同时等待同一个异步结果就绪,应该调用fut.share()将其转化为std::shared_future,它允许多个线程并发多次调用get()。
3. 任务打包器:std::packaged_task¶
在设计线程池或任务调度器时,我们往往需要将“一个函数及其参数”打包暂存起来,扔进队列里,供后续的工作线程拾取执行。std::packaged_task 就是为此而生的神器!
flowchart LR
TaskFunc["可调用函数<br>int add(int a, int b)"] --> PackagedTask["std::packaged_task<int(int, int)><br>自动将函数与一个内部的 promise 绑定打包"]
PackagedTask --> GetFut["task.get_future() -> 拿到提货凭证 std::future"]
PackagedTask --> Execute["在任意工作线程中像普通函数一样调用: task(10, 20)"]
Execute --> ValueSet["底层自动执行 promise.set_value()"]
#include <iostream>
#include <future>
#include <thread>
int compute_heavy(int a, int b) {
return a + b;
}
int main() {
// 将普通函数包装为 packaged_task
std::packaged_task<int(int, int)> task(compute_heavy);
// 提前取走关联的 future
std::future<int> fut = task.get_future();
// 将 task 移交给子线程执行 (注意 packaged_task 同样不可拷贝,只能 move)
std::thread t(std::move(task), 10, 20);
std::cout << "主线程取回结果: " << fut.get() << "\n"; // 输出: 30
t.join();
return 0;
}
4. 极速异步捷径:std::async 的魔力与隐藏暗礁¶
std::async 是 C++ 标准库提供的最高阶异步函数。它连 std::thread 的创建和 join() 都不需要你操心,直接一行代码返回 std::future:
auto fut = std::async(std::launch::async, []() {
return 42;
});
std::cout << fut.get() << "\n"; // 输出 42
然而,很多初学者在深度使用 std::async 时,往往会踩中两大隐蔽的暗礁:
4.1 暗礁一:启动策略 (Launch Policy) 的假异步陷阱¶
std::async 支持三种启动策略参数:
// 策略 1: 强制在新线程中异步并行执行
std::async(std::launch::async, func);
// 策略 2: 延迟惰性求值!根本不启动新线程,直到调用 get() 或 wait() 时,才在当前调用线程中同步执行!
std::async(std::launch::deferred, func);
// 策略 3: 默认不传策略参数 (等价于 std::launch::async | std::launch::deferred)
std::async(func); // ⚠️ 警告: 隐藏行为!
初学者大坑:如果不传策略,C++ 运行时有权根据系统当前 CPU 负载自主抉择。如果当前系统负载高,运行时可能会选择
deferred(延迟同步执行)。这会导致如果你只是等待时间(wait_for),任务可能永远都没开始运行!
生产准则:只要明确需要异步执行,必须显式指定std::launch::async。
4.2 暗礁二:临时变量析构带来的隐式阻塞¶
请仔细阅读下面这看似纯良的两行代码:
void send_email(int user_id) {
// 耗时 3 秒发送邮件...
}
void process() {
// ❌ 灾难!初学者自以为并发发了 2 封邮件,实际却是串行耗时 6 秒!
std::async(std::launch::async, send_email, 101);
std::async(std::launch::async, send_email, 102);
}
为什么没有并发?¶
因为 std::async 返回了一个临时的 std::future。根据 C++ 标准规定:由 std::async 返回的有效 std::future,在其析构函数中会强制阻塞等待后台任务完成!
在上面的代码中,第一行语句执行完毕时,临时 future 立即析构,导致主线程在此卡顿 3 秒;随后才执行第二行语句,再次卡顿 3 秒。代码完全退化成了串行同步!
正确做法:必须用变量显式保存返回的 future 对象¶
void process_fixed() {
// 用变量接收,让两个任务真正并发并行跑起来!
auto f1 = std::async(std::launch::async, send_email, 101);
auto f2 = std::async(std::launch::async, send_email, 102);
// f1 与 f2 在作用域末尾析构时,两者早已经在后台并行完成
}
5. C++ 异步全家族对比一览¶
| 异步工具 | 角色定位 | 适用典型场景 | 是否管理底层线程 |
|---|---|---|---|
std::thread |
最底层的线程原语 | 长驻后台的守护任务、需要精细控制线程属性 | 手动创建与 join |
std::async |
最高级的开箱即用异步函数 | 偶尔需要后台计算某个短时任务并直接取回结果 | 标准库自动调度 |
std::packaged_task |
可调用实体的异步包装器 | 构建高性能线程池的任务队列,打包函数与返回值 | 不创建线程,仅负责打包 |
std::promise |
底层通信管道的写入端 | 复杂的事件通知、跨线程状态同步、非函数模式的结果传递 | 不创建线程,负责管道通信 |
std::future |
异步结果凭证(只读提货单) | 阻塞等待取结果、跨线程异常接收、超时轮询 | 配合 async/promise/task 使用 |