「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
2
3
int foo(int a, int b){
return a + b;
}

我们可以通过 std::async 创建一个异步任务:

1
std::future<int> res = std::async(std::launch::async, foo, 1, 2);

其中 std::launch::asyncstd::async 的执行策略之一——立即执行函数。

然后,我们可以通过调用 std::future::get() 来获取异步任务返回的结果:

1
auto data = res.get();

以下是一个完整的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <future>

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

int main(){
std::future<int> res = std::async(std::launch::async, foo, 1, 2);
auto data = res.get();
std::cout << data << "\n";

return 0;
}

最终的返回结果为 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <future>
#include <thread>

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

int main(){
std::cout << "main(): thread id = " << std::this_thread::get_id() << "\n";
std::future<int> res = std::async(std::launch::async, foo, 1, 2);
auto data = res.get();
std::cout << "finally get the res = " << data << "\n";

return 0;
}

可能的运行结果如下:

1
2
3
main(): thread id = 0xb8
foo(): thread id = 0xbc
finally get the res = 3

可以看出 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <future>
#include <thread>

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

int main(){
std::cout << "main(): thread id = " << std::this_thread::get_id() << "\n";
std::future<int> res = std::async(std::launch::deferred, foo, 1, 2);
auto data = res.get();
std::cout << "finally get the res = " << data << "\n";

return 0;
}

在上面的代码中,仅有 std::async 的执行策略有改动。

可能的运行结果如下:

1
2
3
main(): thread id = 0xc4
foo(): thread id = 0xc4
finally get the res = 3

可以看出,foo() 执行的线程和主线程 id 一样,这时候并没有开启一个新线程来执行异步任务。

2.3.3 默认策略

如果不指定函数调用策略,默认的策略为 std::launch::async | std::launch::deferred

在 C++ 11 std::future 的定义中,std::async 的执行策略是一个枚举类型,std::launch::async1std::launch::deferred2

当采用的是默认的调用策略时,系统自行决定以异步方式进行还是同步方式进行:

  • 资源不紧张情况下,可能就创建新线程来执行异步任务(相当于 std::launch::async
  • 资源紧张,可能不创建新线程并延迟到调用 get()wait() 才执行线程入口函数(相当于 std::launch::deferred)。

不加策略参数也就是如下这种形式

1
std::future<int> res = std::async(foo, 1, 2); // 系统自行决定策略

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
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
#include <iostream>
#include <thread>
#include <future>
#include <chrono>

int foo(int a, int b){
std::cout << "foo(): thread id = " << std::this_thread::get_id() << "\n";
std::cout << "foo(): starts calculating the res..." << "\n";
std::this_thread::sleep_for(std::chrono::seconds(4));
std::cout << "foo(): ends calculating the res..." << "\n";
return a + b;
}

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

// 创建一个异步任务,std::launch::async 会立即在一个子线程中开始执行 foo()
std::future<int> res = std::async(std::launch::async, foo, 3, 4);

// 主线程 main() 中同时可以做一些其他事情
std::cout << "main(): starts doing somethine else..." << "\n";
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "main(): ends doing something else..." << "\n";

// (1) 在主线程中调用 get() 获取结果,如果 foo() 还未执行完,主线程会被阻塞,直到 foo() 执行结束得到结果
auto data = res.get();
std::cout << "finally get the res = " << data << "\n"; // 直到获取结果才会执行这一句
// (2) 如果 foo() 还未执行完,主线程会被阻塞,但是不返回结果
// res.wait();
// (3) 如果前面调用 get() 或 wait(),这句会最后执行;否则,这句会先执行

std::cout << "Kirisame Marisa." << "\n";

return 0;
}

在上面的代码中,foo() 函数是一个需要消耗一定时间的函数(执行时间设定为大约 4 秒)。

主线程中,我们一开始创建了一个异步任务 res,调用策略参数为 std::launch::async 即立即执行。在创建异步任务后立即执行,与此同时,我们在主线程中还模拟做了一些其他事情(执行时间设定为大约 2 秒)。

之后我们采用不同的结果获取机制进行测试。

最后输出一个 Kirisame Marisa 提示主线程进行的所有操作结束⭐。

  1. 如果主线程中调用 get() 获取结果。由于 foo() 还未执行完,主线程会被阻塞,直到 foo() 执行结束才得到结果。

    可能的运行结果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    main(): 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() 的返回结果。

  2. 如果主线程中调用 wait()。由于 foo() 未执行完,主线程会被阻塞,foo() 函数执行完毕后,并没有结果获取。

    可能的运行结果如下:

    1
    2
    3
    4
    5
    6
    7
    main(): 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)。不过并没有获取结果。

  3. 如果主线程中既没有调用 get() 方法或者 wait() 方法。主线程无论如何也不会去等待 foo() 函数执行完毕。在上面的示例中,如果什么也没有调用,那么 Kirisame Marisa 这句话在主函数前面的其他操作完成之后就执行。

    可能的运行结果如下:

    1
    2
    3
    4
    5
    6
    7
    main(): 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
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
#include <iostream>
#include <thread>
#include <future>
#include <chrono>

int foo(int a, int b){
std::cout << "foo(): thread id = " << std::this_thread::get_id() << "\n";
std::cout << "foo(): starts calculating the res..." << "\n";
std::this_thread::sleep_for(std::chrono::seconds(4));
std::cout << "foo(): ends calculating the res..." << "\n";
return a + b;
}

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

// 创建一个异步任务,std::launch::deferred 会延迟执行 foo(),直到某个调用 get() 或 wait()
std::future<int> res = std::async(std::launch::deferred, foo, 3, 4);

// 主线程 main() 中同时可以做一些其他事情
std::cout << "main(): starts doing somethine else..." << "\n";
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "main(): ends doing something else..." << "\n";

// (1) 在主线程中调用 get() 才开始执行 foo(),实际上是在主线程中执行,最后获取结果
auto data = res.get();
std::cout << "finally get the res = " << data << "\n"; // 直到获取结果才会执行这一句
// (2) 在主线程中调用 get() 才开始执行 foo(),但最后不获取结果
// res.wait();
// (3) 如果前面不调用 get() 或 wait(),foo() 压根不会被执行,会直接输出这一句

std::cout << "Kirisame Marisa." << "\n";

return 0;
}

与前面 3.1 std::async 立即执行分析 中的代码不同的是,创建异步任务时采用的调用策略为 std::launch::deferred

  1. 如果在主线程中调用 get()。由于使用了 std::launch::deferred 的参数,异步任务会在调用 get() 这一行才开始执行 foo(),实际上是在主线程中执行,最后获取结果。

    可能的运行结果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    main(): 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 表示程序的结束。

  2. 如果在主线程中调用 wait()。由于使用了 std::launch::deferred 的参数,异步任务会在调用 get() 这一行才开始执行 foo(),且在主线程中执行,只是最后不获取结果。

    可能的运行结果如下:

    1
    2
    3
    4
    5
    6
    7
    main(): 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.

    可以看出,输出的结果和前面差不多,不过就是没有获取结果而已。

  3. 如果主线程中既没有调用 get() 方法或者 wait() 方法。由于使用的时延迟调用参数,加上参数后会在调用 get() 或者 wait() 后执行 foo()。但是我们什么也没有做,所以 foo() 函数压根不会执行。会直接输出结尾的 Kirisame Marisa

    可能的运行结果如下:

    1
    2
    3
    4
    main(): 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
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
46
47
#include <iostream>
#include <thread>
#include <chrono>
#include <future>

int foo(int a, int b){
std::cout << "foo(): thread id = " << std::this_thread::get_id() << "\n";
std::cout << "foo(): starts caculating the res..." << "\n";
std::this_thread::sleep_for(std::chrono::seconds(2)); // foo() 需要执行大概 2 秒
std::cout << "foo(): ends caculating the res..." << "\n";
return a + b;
}

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

// (1) 创建一个异步任务
// (1.1) std::launch::async 会立即执行 foo(),wait_for() 可能超时,可能成功获取结果
std::future<int> res = std::async(std::launch::async, foo, 3, 4);
// (1.2) std::launch::deferred 会延迟执行 foo(),直到某个调用 get() 或 wait(),wait_for() 不起作用
// std::future<int> res = std::async(std::launch::deferred, foo, 3, 4);
// (1.3) 默认情况(std::launch::async | std::launch::deferred),则不确定为 (1.1) 还是 (1.2)
// std::future<int> res = std::async(foo, 3, 4);

auto get_data = [&](std::future<int>& res){
auto data = res.get();
std::cout << "finally get the res = " << data << "\n";
};

// (2) 枚举类型,判断 res 的状态
// std::future_status status = res.wait_for(std::chrono::seconds(1)); // 等待 1 秒,超时
std::future_status status = res.wait_for(std::chrono::seconds(3)); // 等待 3 秒,成功获取结果
if(status == std::future_status::timeout){
// (2.1) foo() 还未执行完成,超过等待时间,没有返回结果,超时
std::cout << "WARNING: time out" << "\n";
}else if(status == std::future_status::ready){
// (2.2) foo() 在等待时间内执行完毕,可以获取结果(参数为 std::launch::async)
std::cout << "successfully get result." << "\n";
get_data(res);
}else if(status == std::future_status::deferred){
// (2.3) 参数为 std::launch::deferred 就会出现此情况
std::cout << "thread is deferred." << "\n";
get_data(res);
}

return 0;
}

在上面的代码中,foo() 需要执行大概 2 秒,我们可以选择 wait_for() 等待 1 秒或者 3 秒。

  1. 如果使用 std::launch::async 作为调用策略:

    • 如果选择等待 1 秒的时间,这个时间是小于异步任务执行 foo() 所需时间的,故此时会超时,进入代码中注释 (2.1) 的情况。

      运行结果如下:

      1
      2
      3
      4
      5
      main(): 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
      6
      main(): 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
  2. 如果使用 std::launch::deferred 作为调用策略:

    此时由于会在调用 get()wait() 才开始执行,因此无论等待多少秒,都会进入 (2.3) 的情况,即 foo() 在调用 get() 时才开始执行,最终获取结果。

    运行结果如下:

    1
    2
    3
    4
    5
    6
    main(): 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.,表明这个时候是延迟执行的。

  3. 如果不增加调用策略参数:

    那么系统会根据实际情况选择策略。此时结果就不确定了。

4 std::async 和 std::thread 区别

相比于 std::threadstd::async 提供了更加高级的异步操作接口。

  • 线程的管理

    std::async 的如线程的创建、销毁以及资源回收等操作是被隐藏的,这种抽象使得程序员可以更专注于任务本身和结果的获取,而不需要深入考虑线程的底层操作。

    std::thread 需要合理的管理线程的生命周期,需要确保线程在对象销毁之前已经正确地完成(std::thread::join())或者分离(std::thread::detach())。

  • 返回值的处理

    std::async 主要用于执行一个有返回值的任务,并通过 std::future 对象方便地获取返回值。

    std::thread 本身没有内置的机制来获取线程函数的返回值。通常我们需要设定一个 共享数据变量,并且使用互斥操作维护变量的值(或者简单的情况下使用原子操作 std::atomic)。

  • 线程执行策略和灵活性

    std::async 有多种执行策略(主要分为 std::launch::asyncstd::launch::deferred),在某些情况下可以根据实际需求来控制任务的执行时机。

    std::thread 一旦启动就会立即执行任务。

总的来说,std::async 适合用于执行一些有返回值的任务,并且希望方便地获取结果的情况;std::thread 适合用于执行一些不需要返回值或者对返回值获取机制要求不高的任务。

参考

  1. std::async

  2. std::future::get()

  3. std::future::wait()

  4. std::future::wait_for()


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