C++ 多线程并发编程4 异步编程

引言

std::thread对象在创建完成时,对应的线程就会开始异步执行,但是如果我们想要得知线程的执行结果,那么就需要手动设计和实现一套同步机制。为此C++引入std::future,提供了一个访问异步操作结果的机制。

1
#include <future>

std::future是一个“未来值”的代理,我们并不直接创建它,而是通过头文件<future>中的其他手段来获取

  1. std::promise: 在某个线程中设置值或异常,然后在另一个线程中通过关联的 std::future 获取。
  2. std::packaged_task: 将一个函数包装起来,使其可以异步调用,并可以通过 std::future 获取其结果
  3. std::async: 最高级的抽象,它自动创建线程(或使用线程池)并返回一个 std::future

std::promise

promise对象在构造时会和一个共享状态相关联,这个共享状态一般为future对象。promise对象可以在关联的共享状态上保存模板类型T的值,这个值由promise对象的成员函数get_future()读取。

需要注意的是,虽然promise对象在构造时就与一个共享状态相关联,但如果要和某个具体的future对象关联,则需要调用get_future()并将结果赋值给这个future对象。

构造函数

1
2
3
4
5
6
7
promise(); //初始化一个空的共享状态。

template <class Alloc> promise (allocator_arg_t aa, const Alloc& alloc);//可以自定内内存分配器

promise (const promise&) = delete;

promise (promise&& x) noexcept;

std::promise::get_future

promise对象设置共享状态上的值或者异常对象后,返回与之相关联的future对象。如果在调用该函数时,promise对象没有设置值或者异常对象(共享状态标志不为ready),promise对象在析构时会自动地设置一个 future_error 异常(broken_promise)来设置其自身的准备状态。

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
#include <iostream>       // std::cout
#include <thread> // std::thread
#include <future> // std::promise, std::future

std::promise<int> prom;

void print_global_promise () {
std::future<int> fut = prom.get_future();
int x = fut.get();
std::cout << "value: " << x << '\n';
}

int main ()
{
std::thread th1(print_global_promise);
prom.set_value(10);
th1.join();

prom = std::promise<int>(); // prom 被move赋值为一个新的 promise 对象.

std::thread th2 (print_global_promise);
prom.set_value (20);
th2.join();

return 0;
}
/*
输出:
value: 10
value: 20
*/

std::promise::set_value

设置共享状态的值,设置成功后 promise 的共享状态标志变为 ready

std::promise::set_exception

promise 设置异常,设置成功后 promise 的共享状态标志变为 ready

异常会在调用std::future::get()时抛出

std::promise::set_value_at_thread_exit

设置共享状态的值,但是不将共享状态的标志设置为 ready,而是在线程退出时将该 promise 对象的共享状态标志自动设置为 ready

在该函数设置了promise对象的共享状态的值后,直到该线程结束之前,如果有其他修改共享状态的值的操作,都会抛出future_error异常。

std::promise::swap

交换两个promise对象的共享状态

std::package_task

std::packaged_task 包装一个可调用的对象,并且允许异步获取该可调用对象产生的结果,并将结果传递给一个std::future,也就是说,std::packaged_task在共享状态上设置的值就是其绑定的函数的返回值。

std::promise对象类似,std::package_task对象使用get_future成员函数返回future对象。

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
#include <iostream>     // std::cout
#include <future> // std::packaged_task, std::future
#include <chrono> // std::chrono::seconds
#include <thread> // std::thread, std::this_thread::sleep_for

// count down taking a second for each value:
int countdown (int from, int to) {
for (int i=from; i!=to; --i) {
std::cout << i << '\n';
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "Finished!\n";
return from - to;
}

int main ()
{
std::packaged_task<int(int,int)> task(countdown); // 设置 packaged_task
std::future<int> ret = task.get_future(); // 获得与 packaged_task 共享状态相关联的 future 对象.

std::thread th(std::move(task), 10, 0); //创建一个新线程完成计数任务.

int value = ret.get(); // 等待任务完成并获取结果.

std::cout << "The countdown lasted for " << value << " seconds.\n";

th.join();
return 0;
}

构造函数

1
2
3
4
5
6
7
8
9
10
11
packaged_task() noexcept;//初始化一个空的共享状态,并且该 packaged_task 对象无包装任务。

template <class Fn>
explicit packaged_task (Fn&& fn);//初始化一个共享状态,并且被包装任务由参数 fn 指定

template <class Fn, class Alloc>
explicit packaged_task (allocator_arg_t aa, const Alloc& alloc, Fn&& fn);//带自定义内存分配器的构造函数,与默认构造函数类似,但是使用自定义分配器来分配共享状态

packaged_task (const packaged_task&) = delete;

packaged_task (packaged_task&& x) noexcept;

std::packaged_task::valid

检查当前 packaged_task 是否和一个有效的共享状态相关联,对于由默认构造函数生成的 packaged_task 对象,该函数返回 false

std::packaged_task::get_future

返回一个与 packaged_task 对象共享状态相关的 future 对象。返回的 future 对象可以获得由另外一个线程在该 packaged_task 对象的共享状态上设置的某个值或者异常。

std::packaged_task::operator()(Args... args)

调用该 packaged_task 对象所包装的对象(通常为函数指针,函数对象,lambda 表达式等),传入的参数为 args. 调用该函数一般会发生两种情况:

  • 如果成功调用 packaged_task 所包装的对象,则返回值(如果被包装的对象有返回值的话)被保存在 packaged_task 的共享状态中。
  • 如果调用 packaged_task 所包装的对象失败,并且抛出了异常,则异常也会被保存在 packaged_task 的共享状态中。
    以上两种情况都使共享状态的标志变为 ready,因此其他等待该共享状态的线程可以获取共享状态的值或者异常并继续执行下去。

共享状态的值可以通过在 future 对象(由 get_future获得)上调用 get 来获得。

由于被包装的任务在 packaged_task 构造时指定,因此调用 operator() 的效果由 packaged_task 对象构造时所指定的可调用对象来决定:

  • 如果被包装的任务是函数指针或者函数对象,调用 std::packaged_task::operator() 只是将参数传递给被包装的对象。
  • 如果被包装的任务是指向类的非静态成员函数的指针,那么 std::packaged_task::operator()第一个参数应该指定为成员函数被调用的那个对象,剩余的参数作为该成员函数的参数。
  • 如果被包装的任务是指向类的非静态成员变量(也就是说),那么 std::packaged_task::operator() 只允许单个参数。

std::packaged_task::make_ready_at_thread_exit

该函数会调用被包装的任务,并向任务传递参数,类似 std::packaged_task::operator() 成员函数。但是不同之处在于,make_ready_at_thread_exit 并不会立即设置共享状态的标志为 ready,而是在线程退出时设置共享状态的标志

如果与该 packaged_task 共享状态相关联的 future 对象在 future::get 处等待,则当前的 future::get 调用会被阻塞,直到线程退出。而一旦线程退出,future::get 调用继续执行,或者抛出异常。

std::promise的对应函数类似,如果在线程结束之前有其他设置或者修改共享状态的值的操作,则会抛出 future_error

std::packaged_task::reset()

重置 packaged_task 的共享状态,但是保留之前的被包装的任务

std::packaged_task::swap()

交换 packaged_task 的共享状态。

std::async

1
2
3
4
5
template <class Fn, class... Args>
future<typename result_of<Fn(Args...)>::type> async(Fn&& fn, Args&&... args);

template <class Fn, class... Args>
future<typename result_of<Fn(Args...)>::type> async(launch policy, Fn&& fn, Args&&... args);
  • std::async()fnargs 参数用来指定异步任务及其参数,函数会返回一个std::future对象。
  • 第二个重载指定了任务的启动策略,包括
    • launch::async 异步任务会立刻另外一个线程中调用,并通过共享状态返回异步任务的结果(一般是调用 std::future::get() 获取异步任务的结果)。
    • launch::deferred 异步任务将会在共享状态被访问时调用,相当与按需调用(即延迟(deferred)调用)。也就是说当使用 launch::deferred 时,标准库并不会马上启动一个新线程去执行 fn(args…),它只是把任务和参数存进一个内部结构里,并创建好与之对应的“共享状态”。只有当通过async返回的 future对象去访问(通过get(),wait()wait_for()wait_until())这个状态时,这个任务才真正被调用。

std::future

正如前面所提到的,std::future是用于获取异步任务结果的类,std::future对象通常由某个Provider创建,std::promisestd::packaged_task都属于异步编程中的Provider类,而std::async则是Provider函数。

一个 std::future 对象只有在有效的情况下才有用,而由 std::future 默认构造函数创建的 future 对象不是有效的(除非当前非有效的 future 对象被另一个有效的 future 对象 move 赋值)。

构造函数

1
2
3
4
5
future() noexcept;

future (const future&) = delete;

future (future&& x) noexcept;

future的赋值构造函数也是被禁用的,只允许使用移动赋值

1
2
future operator=(const future& x) = delete;
future operator=(future&& x) noexcept;

std::future::share()

返回一个 std::shared_future 对象,调用该函数之后,该 std::future 对象本身已经不和任何共享状态相关联,因此该 std::future 的状态不再是 valid 的了。

std::future::get()

1
2
3
4
5
T get();

R& future<R&>::get(); // when T is a reference type (R&)

void future<void>::get(); // when T is void

在一个有效的 future 对象上调用 get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值或异常(此时共享状态的标志变为 ready),std::future::get 将返回异步任务的值或异常(如果发生了异常)。

std::future::valid()

检查当前的 std::future 对象是否有效,即是否与某个共享状态相关联。一个有效的 std::future 对象只能通过 std::async(), std::promise::get_future 或者 std::packaged_task::get_future 来初始化,或者通过移动构造移动赋值从别的有效的std::future对象哪里获取。

另外由 std::future 默认构造函数创建的 std::future 对象是无效的,当然通过 std::futuremove 赋值后该 std::future 对象也可以变为有效的对象。

std::future::wait()

等待与当前std::future 对象相关联的共享状态的标志变为 ready.

如果共享状态的标志不是 ready(此时 Provider 没有在共享状态上设置值(或者异常)),调用该函数会被阻塞当前线程,直到共享状态的标志变为 ready
一旦共享状态的标志变为 readywait() 函数返回,当前线程被解除阻塞,但是 wait()不读取共享状态的值或者异常

std::future::wait_for()

std::future::wait() 不同的是,wait_for() 可以设置一个时间段 rel_time,如果共享状态的标志在该时间段结束之前没有被 Provider 设置为 ready,则调用 wait_for 的线程被阻塞,在等待了 rel_time 的时间长度后 wait_until() 返回,返回值如下

  • future_status::ready 共享状态的标志已经变为 ready,即 Provider 在共享状态上设置了值或者异常。
  • future_status::timeout 超时,即在规定的时间内共享状态的标志没有变为 ready
  • future_status::deferred 共享状态包含一个 deferred 函数。

std::future::wait_until()

std::future::wait_for()不同的是,wait_until()设置的是系统绝对时间点abs_time

std::shared_future

std::shared_futurestd::future 类似,区别在于 std::shared_future 可以拷贝、多个 std::shared_future 可以共享某个共享状态的最终结果(即共享状态的某个值或者异常)。shared_future 可以通过某个 std::future 对象隐式转换(参见 std::shared_future 的构造函数),或者通过 std::future::share() 显示转换,无论哪种转换,被转换的那个 std::future 对象都会变为无效的对象.

1
2
3
4
5
6
7
shared_future() noexcept;

shared_future (const shared_future& x);

shared_future (shared_future&& x) noexcept;

shared_future (future<T>&& x) noexcept;//传入的future对象最后会变为无效的对象

相关的枚举类

std::future_errc

  • broken_promise 0
    与该 std::future 共享状态相关联的 std::promise 对象在设置值或者异常之前被销毁。
  • future_already_retrieved 1
    与该 std::future 对象相关联的共享状态的值已经被当前 Provider 获取了,即调用了 std::future::get 函数。
  • promise_already_satisfied 2
    std::promise 对象已经对共享状态设置了某一值或者异常。
  • no_state 3
    无共享状态。

std::future_status

  • future_status::ready 0
    wait_for(或wait_until) 因为共享状态的标志变为 ready 而返回。
  • future_status::timeout 1
    超时,即wait_for(或wait_until) 因为在指定的时间段(或时刻)内共享状态的标志依然没有变为 ready 而返回。
  • future_status::deferred 2
    共享状态包含了 deferred 函数。

参考


C++ 多线程并发编程4 异步编程
https://guts.homes/2025/06/26/cpp-multiThreading4/
作者
guts
发布于
2025年6月26日
许可协议