C++ 异步任务详解:future, promise, async
目录
1. 核心问题:线程如何“返回”结果?
2. 核心比喻:去咖啡店点单
3. std::promise 与 std::future:手动挡组合
std::promise (承诺者 / 生产者)
std::future (未来凭证 / 消费者)
代码示例:手动管理线程与结果传递
4. std::async:自动挡,一键启动
代码示例:使用 async 简化任务
5. 区别总结:我应该用哪个?
关键决策点:
1. 核心问题:线程如何“返回”结果?
在普通的单线程编程中,我们调用一个函数,它会立即返回一个结果:
int calculate() { return 100; }
int result = calculate(); // 直接拿到结果
但在多线程世界里,情况变得复杂。当我们创建一个 std::thread
来执行一个任务时,主线程不会等待它完成,而是继续往下执行。那么,主线程该如何获取子线程的计算结果呢?
int result_from_thread;
void calculate_in_thread() { // ... 做一些耗时计算 ...result_from_thread = 100; // ? 这样不安全,有数据竞争!
}std::thread t(calculate_in_thread);
// 主线程怎么知道什么时候计算完成?
// 如何安全地拿到那个 100?
t.join();
直接使用共享变量和互斥锁可以做到,但非常繁琐且容易出错。为了优雅地解决这个问题,C++11 引入了 future
、promise
和 async
。
2. 核心比喻:去咖啡店点单
想象一下这个场景,能帮你理解这三个工具的关系:
-
你 (主线程): 想要一杯咖啡(计算结果)。
-
收银员 (Promise): 承诺稍后会给你做好咖啡。他给了你一个取餐小票。
-
取餐小票 (Future): 这是你未来领取咖啡的凭证。你可以拿着它去做别的事,比如看手机。
-
咖啡师 (子线程): 在后台默默地制作咖啡。
-
get()
操作: 你拿着小票去取餐口,如果咖啡好了,你立刻拿到;如果没好,你就在那里等着 (阻塞),直到咖啡师做好递给你。 -
async
(全自动点餐机): 你只需要在机器上点单,它自动下单给后台,并直接吐给你一张取餐小票 (Future)。它把收银员和咖啡师的工作细节都封装起来了。
3. std::promise
与 std::future
:手动挡组合
promise
和 future
是一对底层的、需要手动配合使用的工具。它们像一个一次性的、单向的通信管道。
-
std::promise
:管道的写入端,位于生产者(子线程)中。 -
std::future
:管道的读取端,位于消费者(主线程)中。
std::promise
(承诺者 / 生产者)
它只有一个职责:在未来的某个时间点,往管道里放入一个值或一个异常。
-
set_value(value)
: 成功完成任务,将结果放入管道。 -
set_exception(exception_ptr)
: 任务失败,将一个异常放入管道。 -
get_future()
: 创建promise
后,立即调用此方法获取其配对的future
对象。
std::future
(未来凭证 / 消费者)
它也只有一个职责:等待并获取管道里的值或异常。
-
get()
: 等待直到管道里有东西,然后取出它。这个操作是阻塞的,并且只能调用一次。 -
wait()
: 只等待,不取值。 -
valid()
: 检查这个future
是否与一个通信管道关联。
代码示例:手动管理线程与结果传递
#include <iostream>
#include <thread>
#include <future>
#include <chrono>// 这个函数将在子线程中运行
// 它接收一个 promise 对象,用来在计算完成后设置结果
void compute_task(std::promise<int> p) {try {std::cout << "子线程开始计算..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2));int result = 42;// 模拟可能发生的错误// throw std::runtime_error("计算失败!");// 计算完成,履行承诺,将结果放入 promisep.set_value(result);std::cout << "子线程已设置结果。" << std::endl;} catch (...) {// 如果发生异常,将异常信息放入 promisep.set_exception(std::current_exception());std::cout << "子线程已设置异常。" << std::endl;}
}int main() {// 1. 在主线程创建一个 promise 对象,类型为我们要返回的 intstd::promise<int> my_promise;// 2. 从 promise 中获取配对的 future 对象。这是我们的“取餐小票”std::future<int> result_future = my_promise.get_future();// 3. 创建子线程,并将 promise 的所有权转移(std::move)给它// promise 不能被拷贝,只能被移动std::thread t(compute_task, std::move(my_promise));std::cout << "主线程正在做其他事情..." << std::endl;// ...可以执行其他不依赖于结果的任务...std::cout << "主线程现在需要计算结果了,开始等待..." << std::endl;try {// 4. 在需要结果时,调用 future 的 get() 方法// 如果子线程还没 set_value,这里会阻塞int final_result = result_future.get();std::cout << "主线程成功获取结果: " << final_result << std::endl;} catch (const std::exception& e) {// 如果子线程设置的是异常,get() 会重新抛出它std::cout << "主线程捕获到异常: " << e.what() << std::endl;}t.join();return 0;
}
4. std::async
:自动挡,一键启动
std::async
是一个高级函数模板,它极大地简化了异步任务的创建。它就像前面比喻里的“全自动点餐机”,你只需要告诉它做什么,它会帮你处理好后台的一切,并直接给你“取餐小票”。
它封装了:
-
线程的创建与管理。
-
promise
对象的创建。 -
future
对象的创建和返回。 -
将任务的返回值或异常自动存入
promise
的逻辑。
代码示例:使用 async
简化任务
#include <iostream>
#include <future>
#include <chrono>// 这就是我们要异步执行的任务,它直接返回结果
int long_computation() {std::cout << "异步任务开始计算..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2));// throw std::runtime_error("计算失败!"); // 异常也会被自动捕获return 42;
}int main() {// 1. 调用 std::async,传入要执行的函数// 它立即返回一个 future 对象,后台任务可能已经开始执行// std::launch::async 策略确保任务在新线程中运行std::future<int> result_future = std::async(std::launch::async, long_computation);std::cout << "主线程正在做其他事情..." << std::endl;// ...std::cout << "主线程现在需要计算结果了,开始等待..." << std::endl;try {// 2. 在需要时,调用 future 的 get() 方法获取结果int final_result = result_future.get();std::cout << "主线程成功获取结果: " << final_result << std::endl;} catch (const std::exception& e) {std::cout << "主线程捕获到异常: " << e.what() << std::endl;}return 0;
}
可以看到,使用 std::async
的代码量大大减少,逻辑也更清晰。
5. 区别总结:我应该用哪个?
特性 |
|
|
---|---|---|
抽象层次 | 低层次,提供精细控制。 | 高层次,封装了所有细节。 |
线程管理 | 你必须手动创建和管理 | 它为你自动管理线程 (或延迟执行)。 |
代码复杂度 | 更复杂,需要手动创建 | 非常简单,一行代码即可启动任务并获取 |
核心使用场景 | 当“设置结果”的逻辑与“启动任务”的逻辑分离时。例如,在一个复杂的事件驱动系统中,一个线程触发事件,另一个线程等待该事件并设置结果。或者当你需要自己管理一个线程池时。 | 绝大多数情况:你有一个函数,想让它在后台运行,并稍后获取其返回值。这是最常用、最推荐的方式。 |
异常处理 | 需要在子线程中 | 自动处理。任务函数中的异常会被自动捕获并存储在 |
关键决策点:
-
如果你的需求仅仅是“异步调用一个函数并取回其返回值”,那么 99% 的情况下都应该使用
std::async
。它更简单、更安全、意图更明确。 -
只有当你需要非常精细地控制“承诺”何时以及如何被满足,或者你在自己实现一个线程池或消息队列,并且只需要一个纯粹的“值传递通道”时,才需要回退到使用
std::promise
和std::future
。