当我们希望让后台执行一项耗时计算,等算完再直接拿回函数的 return 返回值时,可以使用 <future> 头文件中的 std::async 和 std::future。相比需要借助外部变量传回结果的 std::thread,这一对工具获取异步返回值要直接得多。
它们配合工作的过程很像去奶茶店点单:调用 std::async() 相当于下单让后台开始制作,当场返回的 std::future 对象就是手里的 “取餐小票”。主线程拿到小票后可以先去忙别的事情,等需要结果时再凭小票调用 get() 把结果取回来,如下图所示。

C++ async 与 future 基本用法
在 C++ 中,使用 std::async 和 std::future 之前,我们需要先通过 #include <future> 引入头文件。
语法:
#include <future>
std::future<类型> fut = std::async(启动策略, 函数名, 参数...);
类型 结果变量 = fut.get();说明:
对于上面的语法,我们需要清楚以下 3 个部分的作用:
- 启动策略:常用的第 1 个参数是
std::launch::async,表示立刻开启一条新线程在后台异步执行函数。 - std::future<类型>:用来接收
std::async()返回的 “结果凭证”,尖括号<>里的类型与异步函数的返回值类型保持一致。如果异步函数没有返回值(即返回类型为void),就写成std::future<void>。 - get():等待异步任务完成并取出返回值。如果后台已经算完,它会立刻返回结果;如果还没算完,当前线程就会停在这一行等待,直到拿到结果为止。
示例 1:异步计算并获取结果
#include <chrono>
#include <future>
#include <iostream>
#include <thread>
int add(int a, int b) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return a + b;
}
int main() {
std::future<int> fut = std::async(std::launch::async, add, 10, 20);
std::cout << "主线程:已发起异步计算,先处理其他工作\n";
int sum = fut.get(); // 等待并取出返回值
std::cout << "主线程:拿到计算结果 = " << sum << "\n";
return 0;
}运行结果如下。
主线程:已发起异步计算,先处理其他工作
主线程:拿到计算结果 = 30分析:
在这个例子中,为了模拟后台耗时计算,我们在 add() 内部调用了前面学过的 sleep_for() 让子线程暂停 100 毫秒。主线程调用 std::async() 发起任务后不会停在原地干等,而是立刻拿到 fut 对象,继续往下打印主线程的提示。
直到执行到 int sum = fut.get(); 时,主线程才正式索要结果,等后台返回 30 后将其赋给 sum。日常写代码时,接收返回值的类型 std::future<int> 也可以直接使用 auto 让编译器自动推导。
注意 调用 std::async() 时,小伙伴们一定要定义变量把返回的 std::future 对象接住。如果不接收返回值,生成的临时 future 对象会在当前语句结束时立刻销毁,并在析构时原地等待后台任务跑完,这样就失去了异步并行的效果。
get()、wait() 与 valid()
除了 get() 之外,std::future 还提供了 wait() 和 valid() 两个常用成员函数。在日常开发中,这 3 个函数的职责有明显区别:
- get():等待并取走结果,且只能调用 1 次,相当于 “凭小票取餐并回收小票”。一旦调用了
get(),结果就会被移走,当前的future对象随之失效,不能再调用第 2 次get()。 - wait():只等待任务完成,不取走结果。它相当于站在柜台前等后台把任务做完,但不会把结果领走。因此调用
wait()返回后,我们后面依然可以调用 1 次get()取出结果。 - valid():检查当前
future对象是否还关联着未取走的有效结果。在调用get()之前返回true;调用get()取走结果之后返回false。
示例 2:future 的状态变化
#include <future>
#include <iostream>
int getVal() {
return 42;
}
int main() {
std::future<int> fut = std::async(std::launch::async, getVal);
std::cout << "刚创建时 valid():" << fut.valid() << "\n";
fut.wait();
std::cout << "wait() 后 valid():" << fut.valid() << "\n";
int val = fut.get();
std::cout << "取出结果:" << val << "\n";
std::cout << "get() 后 valid():" << fut.valid() << "\n";
return 0;
}运行结果如下。
刚创建时 valid():1
wait() 后 valid():1
取出结果:42
get() 后 valid():0分析:
fut.wait(); 只是等待 getVal() 执行完毕,并不会消耗掉结果,因此调用后 fut.valid() 依然输出 1(即 true)。而一旦调用了 fut.get() 把 42 取出来,fut 内部保存的状态就被清空了,再次查看 fut.valid() 就变成了 0(即 false)。
因此,如果后面的代码需要多次用到异步计算的结果,小伙伴们一定要在第 1 次调用 get() 时把返回值存进一个普通变量里重复使用,千万不要对同一个 future 对象连续调用 get()。
// 正确方式
int val = fut.get();
std::cout << val << ", " << val * 2 << "\n";
// 错误方式
int a = fut.get();
int b = fut.get();在上面的错误方式中,第 1 次调用 fut.get() 已经把结果取走并使 fut 失效,第 2 次再调用 fut.get() 时就会直接抛出 std::future_error 异常。
捕获异步任务中的异常
在普通子线程中,如果函数抛出了未捕获的异常,整个程序会直接崩溃终止。那如果通过 std::async 启动的异步函数在后台运行出错、抛出了异常,程序会怎么处理呢?
这是 std::future 非常省心的一点:当异步任务抛出异常时,异常不会立刻导致程序崩溃,而是会被 std::future 收集并保存起来。等到主线程调用 get() 去取结果时,这个异常就会在 get() 所在的位置重新抛出,我们可以直接使用熟悉的 try...catch 语句捕获它。
示例 3:捕获异步任务的异常
#include <future>
#include <iostream>
#include <stdexcept>
int divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("除数不能为 0");
}
return a / b;
}
int main() {
auto fut = std::async(std::launch::async, divide, 10, 0);
try {
int res = fut.get();
std::cout << "计算结果:" << res << "\n";
} catch (const std::runtime_error& e) {
std::cout << "捕获异步任务异常:" << e.what() << "\n";
}
return 0;
}运行结果如下。
捕获异步任务异常:除数不能为 0分析:
在这个例子中,异步函数 divide(10, 0) 因为除数为 0,抛出了标准异常 std::runtime_error。这个异常先被保存在了 fut 关联的共享状态中。当主线程在 try 块内部调用 fut.get() 时,异常被原样重新抛出,并由下方的 catch 块安全捕获。
async 的两种启动策略
在前面的示例中,我们在调用 std::async() 时都在第 1 个参数位置传入了 std::launch::async。实际上,C++ 标准库一共提供了 2 种启动策略:
- std::launch::async:异步执行策略。调用
std::async()时立刻开启一条新线程,在后台并发执行任务。 - std::launch::deferred:延迟执行策略,其中 deferred 是 “推迟” 的意思。调用
std::async()时不创建新线程,也不执行任务,而是一直推迟到后面调用get()或wait()的那一刻,才在当前线程里把函数执行一遍。
示例 4:deferred 延迟执行
#include <future>
#include <iostream>
int lazyTask() {
std::cout << " -> 正在执行 lazyTask()\n";
return 100;
}
int main() {
std::cout << "1. 创建 deferred 任务\n";
auto fut = std::async(std::launch::deferred, lazyTask);
std::cout << "2. 准备调用 fut.get()\n";
int val = fut.get();
std::cout << "3. 拿到结果:" << val << "\n";
return 0;
}运行结果如下。
1. 创建 deferred 任务
2. 准备调用 fut.get()
-> 正在执行 lazyTask()
3. 拿到结果:100分析:
从打印顺序可以清楚地看到:由于创建 fut 时传入了 std::launch::deferred 延迟策略,因此在第 1 步创建任务时,lazyTask() 根本没有运行。对比前面的示例 1,这里一直等到第 2 步之后真正调用 fut.get() 索要结果时,lazyTask() 才在主线程中被触发执行。
需要注意的是,如果在调用 std::async(lazyTask) 时省略了第 1 个策略参数,C++ 标准允许标准库自行决定到底是开启新线程还是推迟执行。为了让程序的并发行为明确可控,在实际开发中,只要希望任务真正在后台异步运行,我们都应该显式写上 std::launch::async。
C++ 常用并发工具选型建议
到这里,我们已经把 C++ 多线程与并发编程中最常用的基础工具都学完了。在实际开发中遇到不同场景时,小伙伴们可以根据下面这 5 条经验来选择:
手动管理线程生命周期:需要启动一条无返回值的后台执行路线时,我们可以使用 std::thread 或 C++20 的 std::jthread。在支持 C++20 的环境下,优先推荐使用自动汇合的 std::jthread。
异步计算并获取返回值:关心后台函数的 return 返回值或异常,且不想手动定义共享变量接结果时,我们可以使用 std::async 配合 std::future。
单个简单变量无锁同步:共享数据只是独立的整数计数器或布尔标志、仅做单步读写或加减时,我们可以优先使用轻便的 std::atomic。
多变量或复杂临界区保护:一次修改涉及多个关联变量、操作容器或包含多步逻辑时,我们应该使用 std::mutex 配合 std::lock_guard 或 std::unique_lock 加锁保护。
等待特定条件成立再执行:需要让线程在条件未满足时睡眠等待、等其他线程改好状态后再发通知唤醒时,我们可以使用 std::condition_variable。
