「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 |
|
其中 R
为可调用对象的返回值的类型;ArgTypes
为可调用对象的参数列表。
1.2 基本用法
-
首先我们需要一个可调用对象,我们可以定义一个简单的两数相加的函数:
1
2
3int foo(int a, int b){
return a + b;
} -
然后通过
std::packaged_task
把这个函数包装起来:1
std::packaged_task<int(int, int)> task(foo);
其中
int(int, int)
是可调用对象的函数签名。 -
然后通过
task.get_future()
获取这个包装任务task
关联的std::future
对象,之后将通过这个对象获取任务执行的结果。1
std::future<int> res = task.get_future();
-
将
std::packaged_task
对象(包装的任务task
)传入一个子线程执行(也可以直接调用这个task
执行):1
2std::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
对象状态也被更新)。如果允许被多次调用,就会出现多个返回值,逻辑会出现混乱。 -
通过
std::future
对象获取结果:1
auto data = res.get(); // 如果包装的异步任务还未完成,主线程被阻塞
以下是完整的代码示例:
1 |
|
运行结果如下:
1 |
|
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)
的效果一样。
可以看出,相当于用 myadd
将 add
函数和参数 a = 1, b = 2
进行绑定,变为一个新的 无参数 的、返回类型为 int
的 myadd
函数。
我们同样可以用到 std::packaged_task
创建中,例如:
1 |
|
2 std::packaged_task 执行方式对比
在 1.2 std::packaged_task 基本用法 中,通过启动一个子线程来执行异步任务,获取返回的结果。当然,也可以直接在主线程中调用 std::packaged_task
对象来执行这个任务。
如果有一个包装的任务如下:
1 |
|
在主线程直接调用的方法为:
1 |
|
可以看出,std::packaged_task
可以手动地控制任务在一个子线程中异步执行,还是就在当前的主线程中同步执行。
以下是一个完整的代码测试示例:
1 |
|
可能的运行结果如下:
1 |
|
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
侧重于对任务的封装和灵活的执行控制。