12. packaged_task与async:封装、策略与性能

好,咱们今天聊聊异步任务的两个核心工具——std::packaged_taskstd::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 知识体系图

下面这张图帮你理清本章的核心逻辑:

异步任务核心机制 std::packaged_task 包装可调用对象 + future std::async 一键启动异步任务 手动控制执行线程 适合线程池、任务队列 自动管理线程 但需指定启动策略 launch::async 立即新线程执行 launch::deferred 延迟到 get() 时执行 选择依据:任务粒度、线程控制需求、性能要求

12.6 实战建议

最后,给你几个我在项目中总结出来的实用建议:

  • 能用 async 就别手动搞 packaged_task + thread——除非你需要线程池。async 的代码可读性高,不容易出错。
  • 明确指定启动策略——别依赖默认行为。我见过线上 bug 就是因为默认策略下任务被 deferred 了,导致系统启动时数据没加载。
  • 注意 future 的析构行为——std::async 返回的 future 在析构时会阻塞等待任务完成(如果任务是 async 策略)。所以别把 future 扔了不管。
  • 轻量任务别用 async——线程创建开销可能比任务本身还大。这种情况下,用线程池 + packaged_task 更划算。

嗯,说白了,packaged_task 是“零件”,async 是“成品”。你需要自己组装就用零件,想省事就用成品。没有绝对的好坏,看场景。

公众号:蓝海资料掘金营,微信 deep3321