12. packaged_task与async:封装、策略与性能
好,咱们今天聊聊异步任务的两个核心工具——std::packaged_task 和 std::async。
说实话,这两个东西刚接触时容易搞混。我自己刚学那会儿,也花了不少时间才理清楚它们各自该用在什么场景。今天我就把它们的区别、用法、以及性能上的坑,一次性给你讲透。
12.1 std::packaged_task:把函数“打包”成任务
std::packaged_task 说白了,就是一个包装器。它把一个可调用对象(函数、lambda、函数对象)包装起来,并且和 std::future 绑定在一起。
你想想看,我们之前用 std::thread 直接启动线程,返回值怎么拿?要么传引用,要么用 std::promise。而 packaged_task 提供了一种更优雅的方式——它自动帮你创建了一个 future,你只管调用任务,结果通过 future 取就行。
核心要点: packaged_task 不自己创建线程,它只是把任务包装好,你可以选择在任何线程上执行它。
#include <iostream>
#include <future>
#include <thread>
int compute(int x, int y) {
return x * y + x / y;
}
int main() {
// 创建一个 packaged_task,绑定 compute 函数
std::packaged_task<int(int, int)> task(compute);
// 获取与 task 关联的 future
std::future<int> result = task.get_future();
// 在另一个线程上执行任务
std::thread t(std::move(task), 10, 3);
// 获取结果
std::cout << "结果: " << result.get() << std::endl;
t.join();
return 0;
}
嗯,这里要注意:packaged_task 是只移动的,不能拷贝。所以传给线程时要用 std::move。我在项目中第一次用的时候忘了 move,编译报错还愣了半天。
12.2 std::async:一键启动异步任务
std::async 就更方便了。它把“创建线程 + 执行任务 + 获取结果”这三步合并成了一步。你传一个函数进去,它自动帮你跑起来,返回一个 future。
#include <iostream>
#include <future>
int main() {
// 直接用 async 启动异步任务
std::future<int> result = std::async(std::launch::async, [](int a, int b) {
return a + b;
}, 5, 7);
std::cout << "异步结果: " << result.get() << std::endl;
return 0;
}
你看,代码简洁多了。但简洁背后有门道——启动策略的选择,直接影响性能和行为。
12.3 启动策略:async vs deferred
std::async 有两个启动策略,通过 std::launch 枚举来控制:
| 策略 | 含义 | 行为 |
|---|---|---|
std::launch::async |
强制异步 | 立即在新线程上执行 |
std::launch::deferred |
延迟执行 | 在调用 get() 或 wait() 时,在调用线程上同步执行 |
| 默认(不指定) | 由实现决定 | 可能是 async,也可能是 deferred |
我曾经踩过的坑: 默认策略下,如果你不调用 get() 或 wait(),任务可能永远不会执行!因为实现可能选择了 deferred。所以,如果你需要确保任务一定在另一个线程上跑,请明确指定 std::launch::async。
// 明确指定异步执行
auto f1 = std::async(std::launch::async, []{ /* 耗时操作 */ });
// 延迟执行,直到 get() 才跑
auto f2 = std::async(std::launch::deferred, []{ /* 可能不跑 */ });
// 默认策略——小心!
auto f3 = std::async([]{ /* 不确定何时执行 */ });
12.4 性能分析:什么时候该用哪个?
好,咱们来点实际的。我在项目中做过一些性能测试,这里给你分享几个关键结论。
12.4.1 packaged_task vs async 的开销
packaged_task 更底层,它只负责包装任务,不负责线程管理。如果你需要精细控制线程池、任务队列,用 packaged_task 更合适。
async 更上层,它内部会帮你管理线程的生命周期。但代价是——每次调用 async 都可能创建新线程,如果任务很轻量,线程创建的开销可能比任务本身还大。
我的建议: 如果任务是 CPU 密集型的,且执行时间超过 1 毫秒,用 async 没问题。如果任务很轻量(几十微秒),建议用线程池 + packaged_task,避免频繁创建线程。
12.4.2 延迟执行 vs 立即执行
deferred 策略看起来省了线程,但有个问题——如果你在 get() 之前做了很多其他工作,任务就一直没跑,最后 get() 时阻塞很久。这其实把并行变成了串行。
我遇到过这样的情况:一个后台数据加载任务用了 deferred,结果主线程忙别的事,数据一直没加载,等到要用时 get() 卡了 2 秒。后来改成 async,数据在后台提前加载好,用户体验好多了。
12.4.3 性能对比表
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 简单异步调用,不关心线程管理 | std::async |
代码简洁,开箱即用 |
| 需要将任务加入线程池 | std::packaged_task |
可移动,灵活绑定到任意执行者 |
| 任务轻量,高频调用 | 线程池 + packaged_task | 避免线程创建销毁的开销 |
| 任务耗时,且需要并行 | std::async(async) |
简单高效,系统自动调度 |
| 延迟计算,按需执行 | std::async(deferred) |
节省资源,但注意使用时机 |
12.5 知识体系图
下面这张图帮你理清本章的核心逻辑:
12.6 实战建议
最后,给你几个我在项目中总结出来的实用建议:
- 能用 async 就别手动搞 packaged_task + thread——除非你需要线程池。async 的代码可读性高,不容易出错。
- 明确指定启动策略——别依赖默认行为。我见过线上 bug 就是因为默认策略下任务被 deferred 了,导致系统启动时数据没加载。
- 注意 future 的析构行为——
std::async返回的future在析构时会阻塞等待任务完成(如果任务是 async 策略)。所以别把 future 扔了不管。 - 轻量任务别用 async——线程创建开销可能比任务本身还大。这种情况下,用线程池 + packaged_task 更划算。
嗯,说白了,packaged_task 是“零件”,async 是“成品”。你需要自己组装就用零件,想省事就用成品。没有绝对的好坏,看场景。