「C++ 多线程」std::packaged_task 异步任务创建

文章大图来源:pixiv_id=104169949

1 std::packaged_task

1.1 基本概念

std::packaged_task 是 C++ 11 引入的一个模板类,定义在 <future> 头文件中。

std::packaged_task 将一个 可调用对象(如函数、函数对象、lambda 表达式等)包装起来,以便异步执行,并且可以通过关联的 std::future 对象来获取这个可调用对象的返回值。

其基本函数原型为:

1
2
template< class R, class ...ArgTypes >
class packaged_task<R(ArgTypes...)>;

其中 R 为可调用对象的返回值的类型;ArgTypes 为可调用对象的参数列表。

1.2 基本用法

  1. 首先我们需要一个可调用对象,我们可以定义一个简单的两数相加的函数:

    1
    2
    3
    int foo(int a, int b){
    return a + b;
    }
  2. 然后通过 std::packaged_task 把这个函数包装起来:

    1
    std::packaged_task<int(int, int)> task(foo);

    其中 int(int, int) 是可调用对象的函数签名。

  3. 然后通过 task.get_future() 获取这个包装任务 task 关联的 std::future 对象,之后将通过这个对象获取任务执行的结果。

    1
    std::future<int> res = task.get_future();
  4. std::packaged_task 对象(包装的任务 task)传入一个子线程执行(也可以直接调用这个 task 执行):

    1
    2
    std::thread t(std::ref(task), 1, 2); 
    t.join();

    在上面传入子线程执行中,传入的是 std::packaged_task 的引用,也可以使用 std::move 移动语义。当 std::packaged_task 被调用时,它会执行包装的可调用对象。

    需要注意的是:同一个 std::packaged_task 对象 只能被调用一次std::packaged_task 是一个可移动但不可复制的类型,在执行完成后,其内部状态会发生改变(变成任务已完成的状态,关联的 std::future 对象状态也被更新)。如果允许被多次调用,就会出现多个返回值,逻辑会出现混乱。

  5. 通过 std::future 对象获取结果:

    1
    auto data = res.get(); // 如果包装的异步任务还未完成,主线程被阻塞

以下是完整的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <thread>
#include <future>

int foo(int a, int b){
return a + b;
}

int main(){
std::packaged_task<int(int, int)> task(foo); // 包装一个异步任务
std::future<int> res = task.get_future(); // 获取关联的 future 对象

// 创建一个子线程执行异步任务
std::thread t(std::ref(task), 1, 2);
t.join();

// 通过 get() 获取结果
auto data = res.get();
std::cout << "finally get the res = " << data << "\n";

return 0;
}

运行结果如下:

1
finally get the res = 3

1.3 使用 std::bind

std::bind 是一个函数模板,用于将 可调用对象(函数、成员函数指针、lambda 表达式等)与参数 进行 绑定。它返回一个新的可调用对象,这个新对象可以在调用时自动将绑定的参数传递给原始的可调用对象。

假设有一个函数 int add(int a, int b),那么可以使用 std::bind 来创建一个新的可调用对象,如 auto myadd = std::bind(add, 1, 2)。这里 myadd 是一个新的可调用对象,之后使用 myadd() 进行调用,和调用 add(1, 2) 的效果一样。

可以看出,相当于用 myaddadd 函数和参数 a = 1, b = 2 进行绑定,变为一个新的 无参数 的、返回类型为 intmyadd 函数。

我们同样可以用到 std::packaged_task 创建中,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int add(int a, int b){ 
return a + b;
}

// 通过 bind 创建包装任务
std::packaged_task<int()> task(std::bind(add, 1, 2));
std::future<int> res = task.get_future();

// 执行任务
task();

// 通过 get() 获取结果
auto data = res.get();
std::cout << "finally get the res = " << data << "\n";

2 std::packaged_task 执行方式对比

1.2 std::packaged_task 基本用法 中,通过启动一个子线程来执行异步任务,获取返回的结果。当然,也可以直接在主线程中调用 std::packaged_task 对象来执行这个任务。

如果有一个包装的任务如下:

1
2
3
4
5
int foo(int a, int b){
return a + b;
}

std::packaged_task<int(int, int)> task(foo);

在主线程直接调用的方法为:

1
task(1, 2); // 传入 foo() 函数参数即可

可以看出,std::packaged_task 可以手动地控制任务在一个子线程中异步执行,还是就在当前的主线程中同步执行。

以下是一个完整的代码测试示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <thread>
#include <chrono>
#include <future>

int foo(int a, int b){
std::cout << "foo() starts, thread id = " << std::this_thread::get_id() << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
std::cout << "foo() ends, thread id = " << std::this_thread::get_id() << "\n";
return a + b;
}

void run_in_thread(){
std::packaged_task<int(int, int)> pt(foo); // 包装一个异步任务
std::future<int> res = pt.get_future(); // 获取 pt 关联的 future 对象

// 开启一个子线程,调用对象包装引用,启动异步任务
std::thread t(std::ref(pt), 3, 4);
t.join();

// 获取 packaged_task 的结果
int data = res.get();
std::cout << "run_in_thread(): get the res = " << data << "\n";
}

void run_in_main(){
std::packaged_task<int(int, int)> pt(foo); // 包装一个异步任务
std::future<int> res = pt.get_future(); // 获取 pt 关联的 future 对象

// 直接执行任务
pt(1, 2); // 传入参数

// 获取 packaged_task 的结果
int data = res.get();
std::cout << "run_in_main(): get the res = " << data << "\n";
}

int main(){
std::cout << "main() starts, thread id = " << std::this_thread::get_id() << "\n";

run_in_main();
run_in_thread();

return 0;
}

可能的运行结果如下:

1
2
3
4
5
6
7
main() starts, thread id = 0xb8
foo() starts, thread id = 0xb8
foo() ends, thread id = 0xb8
run_in_main(): get the res = 3
foo() starts, thread id = 0xbc
foo() ends, thread id = 0xbc
run_in_thread(): get the res = 7

3 std::packaged_task 与 std::async

3.1 概念区别

std::packaged_task 是一个类模板,它主要用于将一个可调用对象(如函数、函数对象、lambda 表达式等)包装起来,方便异步调用并通过关联的 std::future 对象获取返回结果。

std::async 是一个函数模板,主要用于简化异步任务的启动。它会自动创建一个线程(或者在某些情况下可能不会创建新线程,比如使用 std::launch::deferred 策略)来执行一个可调用对象,并返回一个std::future 对象,用于获取任务的结果。

3.2 执行策略区别

对于 std::packaged_task,需要手动管理任务的执行。可以将 packaged_task 对象传递给一个子线程 std::thread 来启动任务执行,也可以将其放入一个线程池等其他执行环境中,或者在当前线程中直接调用。

std::async 的执行策略由其第一个参数(std::launch 类型)决定。其中 std::launch::async 表示一定会在一个新线程中异步执行任务;std::launch::deferred 表示任务会被延迟执行。「C++ 多线程」std::async 异步任务创建

3.3 使用场景

总的来说,std::async 更侧重于简单快速地开启一个异步任务,并且在不需要深入控制任务执行细节的场景下获取结果;std::packaged_task 侧重于对任务的封装和灵活的执行控制。

参考

  1. std::packaged_task

「C++ 多线程」std::packaged_task 异步任务创建
https://marisamagic.github.io/2025/01/04/20250104/
作者
MarisaMagic
发布于
2025年1月4日
许可协议