「C++ 多线程」std::async 异步任务创建
文章大图来源:pixiv_id=115126697
1 异步任务
异步任务:一个任务的执行与当前线程的执行流程不同步。
在传统的同步问题中,调用一个函数需要等待其执行完毕并返回结果后,在继续下面的操作。然而,异步任务则是在调用函数后,调用者无需等待函数执行完毕,可以继续执行其他的一些操作。
在现实场景中,假如我要准备今天的晚餐,需要做很多道菜(虽然我其实一点都不会)。同步任务就是需要每道菜按照顺序制作,做完一道菜才能做下一道;而异步任务就是我可以在做某些需要长时间炖煮的菜时,同时可以去炒几个其他的菜,不需要等待一道菜完全做好才开始做其他的菜。
2 std::async
2.1 基本概念
std::async
是 C++ 11 引入的一个函数模板,用于异步地启动一个函数任务。它位于 <future>
头文件中。
std::async
允许程序在一个单独的子线程中执行某个函数,并将函数的结果进行返回,而且不会阻塞当前线程的执行。std::async
可以理解为是一种新的 std::thread
,并且相比于传统的 std::thread
,其管理更加简单。
std::async
的返回类型是一个 std::future
类型。std::future
是 C++ 11 中引入的一个模板类,位于 <future>
头文件中,用于处理异步操作的结果。
2.2 基本用法
基本的语法为 std::future<函数返回值类型> async(函数调用策略, 函数名, 函数参数列表);
例如我们有一个简单的函数 foo()
如下:
1 |
|
我们可以通过 std::async
创建一个异步任务:
1 |
|
其中 std::launch::async
是 std::async
的执行策略之一——立即执行函数。
然后,我们可以通过调用 std::future::get()
来获取异步任务返回的结果:
1 |
|
以下是一个完整的简单示例:
1 |
|
最终的返回结果为 3
。在上面的代码中,创建了一个异步任务 res
,所执行的策略为 std::launch::async
(表示立即执行),执行的函数为 foo()
。之后,调用 get()
函数获取异步任务的结果,并最终输出结果。
需要注意的是,std::future
对象 只能调用一次 get()
获取结果。一旦获取了结果,std::future
对象的内部状态就会从 “未完成” 变为 “已完成”。如果尝试对已经调用过 get()
的 std::future
对象再次调用 get()
,程序的行为是未定义的。
2.3 std::async 函数调用策略
2.3.1 std::launch::async
std::launch::async
策略会使得异步任务 立即 在 一个新的子线程 中 开始执行。在 2.2 std::async 基本用法 中的简单代码示例中,采用的就是这个策略。
我们可以在前面的示例中增加打印线程 id 的步骤:
1 |
|
可能的运行结果如下:
1 |
|
可以看出 foo()
在一个新的子线程中立即执行。
2.3.2 std::launch::deferred
std::launch::deferred
策略会使得异步任务 延迟 执行函数,直到某个地方调用了 std::future::get()
或者 std::future::wait()
函数。
其中 std::future::get()
函数调用后 会获取异步任务结果;std::future::wait()
函数调用后则 不会获取结果。
当使用的是 std::launch::deferred
延迟执行的策略时,异步任务延迟到调用 get()
或 wait()
再执行,此时 不会开启一个新的线程 来执行这个函数,因此此时本质上并没有多线程。
以下是一个简单的示例:
1 |
|
在上面的代码中,仅有 std::async
的执行策略有改动。
可能的运行结果如下:
1 |
|
可以看出,foo()
执行的线程和主线程 id 一样,这时候并没有开启一个新线程来执行异步任务。
2.3.3 默认策略
如果不指定函数调用策略,默认的策略为 std::launch::async | std::launch::deferred
。
在 C++ 11 std::future
的定义中,std::async
的执行策略是一个枚举类型,std::launch::async
为 1
,std::launch::deferred
为 2
。
当采用的是默认的调用策略时,系统自行决定以异步方式进行还是同步方式进行:
- 资源不紧张情况下,可能就创建新线程来执行异步任务(相当于
std::launch::async
) - 资源紧张,可能不创建新线程并延迟到调用
get()
或wait()
才执行线程入口函数(相当于std::launch::deferred
)。
不加策略参数也就是如下这种形式
1 |
|
2.4 std::future 不同获取结果机制
2.4.1 std::future::get()
std::future::get()
调用之后,会阻塞当前的线程,直到异步任务完成并返回结果。
注意:std::future::get()
只能调用一次。在调用 get()
获取结果之后,此时结果已经被取出,其内部的状态已经改变。这种状态是不可逆的。
在 2.3.1 std::launch::async 的示例中,在主线程中调用 get()
函数后,有两种情况:
- 可能异步任务执行的比较快,那么会立即获得结果;
- 可能异步任务还没有执行完,此时 主线程会进入阻塞状态,等待异步任务完成后,才能继续获取结果。
不同的是,在 2.3.2 std::launch::deferred 的示例中,使用了 std::launch::deferred
的延迟调用参数。主线程调用 get()
之后,由于并没有创建新的线程执行 foo()
函数,因此实际上还是在主线程中执行,最终获取结果。
2.4.2 std::future::wait()
std::future::wait()
调用之后,会阻塞当前线程,直到异步任务完成,但是不返回结果。主要用于需要等待任务完成但不需要立即获取结果的情况下使用。
与 std::future::get()
不同的是,std::future::wait()
是可以重复调用的。
在 2.3.1 std::launch::async 的示例中,在主线程中调用 wait()
函数后,可能异步任务执行的比较快,主线程不需要等待(不会被阻塞);也可能异步任务还未执行完。主线程进入阻塞状态,直到异步任务执行结束。但是,两种情况都不会即刻返回结果。
不同的是,在 2.3.2 std::launch::deferred 的示例中,使用了 std::launch::deferred
的延迟调用参数。主线程调用 wait()
之后,并没有创建新的线程,实际上还是在主线程中执行,只是最终暂时不获取结果。
3 std::async 具体分析
3.1 std::async 立即执行分析
以下是一个完整的 std::async
立即执行 的示例:
1 |
|
在上面的代码中,foo()
函数是一个需要消耗一定时间的函数(执行时间设定为大约 4
秒)。
主线程中,我们一开始创建了一个异步任务 res
,调用策略参数为 std::launch::async
即立即执行。在创建异步任务后立即执行,与此同时,我们在主线程中还模拟做了一些其他事情(执行时间设定为大约 2
秒)。
之后我们采用不同的结果获取机制进行测试。
最后输出一个 Kirisame Marisa
提示主线程进行的所有操作结束⭐。
-
如果主线程中调用
get()
获取结果。由于foo()
还未执行完,主线程会被阻塞,直到foo()
执行结束才得到结果。可能的运行结果如下:
1
2
3
4
5
6
7
8main(): thread id = 0xcc
main(): starts doing somethine else...
foo(): thread id = 0xd0
foo(): starts calculating the res...
main(): ends doing something else...
foo(): ends calculating the res...
finally get the res = 7
Kirisame Marisa.可以看出,主线程在模型执行其他某些操作的同时,子线程也在执行
foo()
函数,当main()
中模拟其他操作执行结束后等待了一段时间(大约2
秒),foo()
函数执行才结束,然后获取foo()
的返回结果。 -
如果主线程中调用
wait()
。由于foo()
未执行完,主线程会被阻塞,foo()
函数执行完毕后,并没有结果获取。可能的运行结果如下:
1
2
3
4
5
6
7main(): thread id = 0x5c
main(): starts doing somethine else...
foo(): thread id = 0xb4
foo(): starts calculating the res...
main(): ends doing something else...
foo(): ends calculating the res...
Kirisame Marisa.可以看出,主线程确实也等待了子线程执行
foo()
函数结束(最后才输出Kirisame Marisa
)。不过并没有获取结果。 -
如果主线程中既没有调用
get()
方法或者wait()
方法。主线程无论如何也不会去等待foo()
函数执行完毕。在上面的示例中,如果什么也没有调用,那么Kirisame Marisa
这句话在主函数前面的其他操作完成之后就执行。可能的运行结果如下:
1
2
3
4
5
6
7main(): thread id = 0xc4
main(): starts doing somethine else...
foo(): thread id = 0xc8
foo(): starts calculating the res...
main(): ends doing something else...
Kirisame Marisa.
foo(): ends calculating the res...可以看出,主线程没有等待
foo()
函数执行完毕就输出了Kirisame Marisa
,这表明主线程没有被阻塞。
3.2 std::async 延迟执行分析
以下是一个完整的 std::async
延迟执行 的示例:
1 |
|
与前面 3.1 std::async 立即执行分析 中的代码不同的是,创建异步任务时采用的调用策略为 std::launch::deferred
。
-
如果在主线程中调用
get()
。由于使用了std::launch::deferred
的参数,异步任务会在调用get()
这一行才开始执行foo()
,实际上是在主线程中执行,最后获取结果。可能的运行结果如下:
1
2
3
4
5
6
7
8main(): thread id = 0xc0
main(): starts doing somethine else...
main(): ends doing something else...
foo(): thread id = 0xc0
foo(): starts calculating the res...
foo(): ends calculating the res...
finally get the res = 7
Kirisame Marisa.可以看出,主线程 id 和 执行
foo()
的线程 id 一样。在主线程中模拟的其他操作结束之后,foo()
函数才开始执行,相当于是一个串行的程序。最终获取结果,结尾输出Kirisame Marisa
表示程序的结束。 -
如果在主线程中调用
wait()
。由于使用了std::launch::deferred
的参数,异步任务会在调用get()
这一行才开始执行foo()
,且在主线程中执行,只是最后不获取结果。可能的运行结果如下:
1
2
3
4
5
6
7main(): thread id = 0xbc
main(): starts doing somethine else...
main(): ends doing something else...
foo(): thread id = 0xbc
foo(): starts calculating the res...
foo(): ends calculating the res...
Kirisame Marisa.可以看出,输出的结果和前面差不多,不过就是没有获取结果而已。
-
如果主线程中既没有调用
get()
方法或者wait()
方法。由于使用的时延迟调用参数,加上参数后会在调用get()
或者wait()
后执行foo()
。但是我们什么也没有做,所以foo()
函数压根不会执行。会直接输出结尾的Kirisame Marisa
。可能的运行结果如下:
1
2
3
4main(): thread id = 0xb4
main(): starts doing somethine else...
main(): ends doing something else...
Kirisame Marisa.
3.3 std::async 延迟一段时间执行(wait_for())
std::future::wait_for()
是 std::future
类中的一个成员函数,用于等待与 std::future
对象关联的异步任务完成。它会阻塞当前线程,直到满足以下条件之一:
- 等待时间超时,函数返回,不会获取到结果。线程继续执行其他操作。
- 在 等待时间期限内 异步任务完成,此时可以通过
std::future::get()
方法获取结果。
参数:std::future::wait_for()
接受的是一个 std::chrono::duration
类型的时间间隔参数。
返回值:返回 std::future_status
类型的枚举值(是一个枚举类型),该枚举有以下三种可能的值:
-
std::future_status::ready
:表示与std::future
关联的异步任务已经完成,此时可以调用get()
方法获取结果。 -
std::future_status::timeout
:表示 等待超时,即超过了指定的等待时间期限,但异步任务还没有完成。 -
std::future_status::deferred
:当std::async
采用std::launch::deferred
调用策略启动异步任务时,异步任务会在调用get()
或wait()
方法时才执行。std::future_status::deferred
的情况意味着 任务尚未开始执行。
以下是一个完整的 wait_for()
的示例:
1 |
|
在上面的代码中,foo()
需要执行大概 2
秒,我们可以选择 wait_for()
等待 1
秒或者 3
秒。
-
如果使用
std::launch::async
作为调用策略:-
如果选择等待
1
秒的时间,这个时间是小于异步任务执行foo()
所需时间的,故此时会超时,进入代码中注释(2.1)
的情况。运行结果如下:
1
2
3
4
5main(): thread id = 0xac
foo(): thread id = 0xbc
foo(): starts caculating the res...
WARNING: time out
foo(): ends caculating the res...可以看出,等待超时了,不过异步任务还是会将
foo()
执行结束。 -
如果选择等待
3
秒的时间,这个时间是大于异步任务执行foo()
所需时间的,故此时能够在等待的时间期限内获取结果(情况(2.2)
)。运行结果如下:
1
2
3
4
5
6main(): thread id = 0xc8
foo(): thread id = 0xcc
foo(): starts caculating the res...
foo(): ends caculating the res...
successfully get result.
finally get the res = 7
-
-
如果使用
std::launch::deferred
作为调用策略:此时由于会在调用
get()
或wait()
才开始执行,因此无论等待多少秒,都会进入(2.3)
的情况,即foo()
在调用get()
时才开始执行,最终获取结果。运行结果如下:
1
2
3
4
5
6main(): thread id = 0xc8
thread is deferred.
foo(): thread id = 0xc8
foo(): starts caculating the res...
foo(): ends caculating the res...
finally get the res = 7可以看出,在
foo()
被执行之前,条件判断输出thread is deferred.
,表明这个时候是延迟执行的。 -
如果不增加调用策略参数:
那么系统会根据实际情况选择策略。此时结果就不确定了。
4 std::async 和 std::thread 区别
相比于 std::thread
,std::async
提供了更加高级的异步操作接口。
-
线程的管理
std::async
的如线程的创建、销毁以及资源回收等操作是被隐藏的,这种抽象使得程序员可以更专注于任务本身和结果的获取,而不需要深入考虑线程的底层操作。std::thread
需要合理的管理线程的生命周期,需要确保线程在对象销毁之前已经正确地完成(std::thread::join()
)或者分离(std::thread::detach()
)。 -
返回值的处理
std::async
主要用于执行一个有返回值的任务,并通过std::future
对象方便地获取返回值。std::thread
本身没有内置的机制来获取线程函数的返回值。通常我们需要设定一个 共享数据变量,并且使用互斥操作维护变量的值(或者简单的情况下使用原子操作std::atomic
)。 -
线程执行策略和灵活性
std::async
有多种执行策略(主要分为std::launch::async
和std::launch::deferred
),在某些情况下可以根据实际需求来控制任务的执行时机。std::thread
一旦启动就会立即执行任务。
总的来说,std::async
适合用于执行一些有返回值的任务,并且希望方便地获取结果的情况;std::thread
适合用于执行一些不需要返回值或者对返回值获取机制要求不高的任务。