C++并发编程 5:异步任务
C++11引入了std::future和std::promise,为异步编程提供了更结构化的工具。
- 通过std::async或std::packaged_task启动异步任务。
- 通过std::promise则用于设置异步操作的结果。
- 通过std::future获取异步操作的结果。
异步任务的结果
future
std::future是C++的一种模板类,它用于表示异步操作的结果。可用于获取异步任务的返回值或者等待异步任务完成。
一个有效的std::future对象通常由以下三种Provider创建,并和某个共享状态相关联:
- std::async()
- std::packaged_task::get_future()
- std::promise::get_future()
共享状态
共享状态(shared state)是一个内部容器,用于管理异步操作的状态,包括结果值、异常信息等。这个容器在两个或多个线程之间共享。std::future内部持有指向这个共享状态的指针,可以理解是共享状态对象的接口或句柄。
- 共享状态对象保存线程函数及其参数、返回值以及新线程状态等信息。该对象通常创建在堆上,由Provider提供,并交由future管理。
- Provider将计算结果写入共享状态对象,而future通过get()函数来读取该结果。
- 共享状态对象作为异步结果的传输通道,future可以从中方便地获取线程函数的返回值。
- 共享状态内部保存着一个引用计数,当引用计数为0时共享状态才会被释放。
构造函数
- 支持无参默认构造,此对象没有共享状态,因此它是无效的,但是可以通过移动赋值的方式将一个有效的future值赋值给它。
- 支持移动构造。
- 不支持拷贝构造。
get()
get()函数的三个版本:
T get();
T& get();
void get();
get()用于获取异步操作的结果:
- 当共享状态就绪时,返回存储在共享状态中的值(或抛出异常)。
- 如果共享状态尚未就绪(即Provider尚未设置其值或异常),则该函数将阻塞调用线程直到就绪。
- 当共享状态就绪后,则该函数将解除阻塞并返回(或抛出异常),释放其共享状态,这使得future对象不再有效,因此对于每一个future的共享状态,该函数最多应被调用一次。
- void get()不返回任何值,但仍等待共享状态就绪并释放它。
get()函数是通过移动语义将异步结果从future中转移给get的返回值,因此该函数只能被调用一次,同时也意味着这个future对象也不可再使用(valid()为false)。
可以通过查询future的状态:
- ready:
- timeout:
- deferred:
wait()
wait()函数等待共享状态就绪,但不获取异步操作的结果。
- 等待共享状态就绪。
- 如果共享状态尚未就绪,则该函数将阻塞调用的线程直到就绪。
- 当共享状态就绪后,则该函数将解除阻塞并返回。
wait()函数不改变future对象的共享状态。
除了wait()外,wait_for()和 wait_until()函数用于带超时的等待,返回值:
- std::future_status::ready: 共享状态已准备好,异步操作已完成。
- std::future_status::timeout: 在指定的时间内,共享状态没有准备好。
- std::future_status::deferred: 任务被延迟执行。
valid()
valid()检查共享状态的有效性,返回当前的future对象是否与共享状态关联。
share()
share()函数用于将一个独占所有权的std::future转换为一个可以共享的std::shared_future。
当调用future的share()函数时,将创建一个shared_future对象,同时原来的future将失去对共享状态对象的所有权,future内部持有的对共享状态的所有权被移动(move)到新创建的std::shared_future对象中。此后future对象处于无效状态,不能被使用(其valid()为false)。
shared_future
std::shared_future同样用于表示异步操作的结果。与std::future的区别:
- std::future独享共享状态的所有权,而std::shared_future则共享所有权。
- std::future是只移动类型,而std::shared_future既可移动也可复制。
- std::future的get()函数只能调用一次,而std::shared_future的get()可以多次被调用。
共享状态对象内部维护着一个引用计数器。当调用share()创建std::shared_future对象时,该共享状态的引用计数为1,当复制一个std::shared_future对象时,引用计数加一,当析构一个std::shared_future对象时,引用计数减一,当引用计数变为0时,该共享状态对象会被自动释放。
| 特性 | std::future | std::shared_future | 
|---|---|---|
| 典型场景 | 一个生产者线程和一个消费者线程。当生产者完成任务后,只有一个消费者需要获取结果。 | 一个生产者线程和多个消费者线程。多个消费者(可能在不同线程上)都需要获取同一个结果。 | 
| get()调用 | 只能调用一次。 | 可以调用多次。 | 
| 生命周期 | 当 get()被调用且共享状态被访问后,对象通常会失效。 | 引用计数管理。只有当所有关联的 std::shared_future副本都被销毁后,共享状态才会被释放。 | 
| 创建方式 | 通常由 std::async、std::promise::get_future或std::packaged_task::get_future返回。 | 必须通过 std::future::share()方法从std::future转换而来。 | 
创建异步任务
async
async的两个版本:
std::future<> async( F&& f, Args&&... args );
std::future<> async(std::launch policy, F&& f, Args&&... args );
policy表示启动策略:
- std::launch::async: 确保函数在新的线程中执行。
- std::launch::deferred: 函数的执行被延迟,直到调用- future对象的- get()或- wait()方法才开始执行,并且调用- get()或- wait()的线程会被阻塞,直到函数执行完成。
- std::launch::async | std::launch::deferred: 让系统决定是否启动新线程,这是默认行为。
packaged_task
std::packaged_task将任何可调用对象封装成一个task,使得能异步调用它,其返回值或所抛异常被存储于能通过std::future对象访问的共享状态中。简言之,std::packaged_task将一个普通的可调用函数对象转换为异步执行的任务。
std::packaged_task不会自己启动,你必须调用它。
构造函数
- 支持无参构造,创建一个空的packaged_task对象,无共享状态。
- 支持可调用对象的构造,将一个可调用对象封装成一个packaged_task对象。
- 支持移动构造,不支持拷贝构造。
- 支持移动赋值,不支持拷贝赋值。
成员函数
- get_future():
    - 返回一个与packaged_task对象的共享状态关联的std::future对象。
- 对于每一个 packaged_task,只能调用一次。。
 
- operator():
    - 调用该packaged_task对象所包装的可调用对象。
- 返回值被保存在与该packaged_task关联的的共享状态中。
 
- make_ready_at_thread_exit():调用该packaged_task对象所包装的可调用对象, 但是并不会立即设置共享状态的标志为ready,而是在线程退出时才设置共享状态的标志为ready。
- reset(): 重置 packaged_task 的共享状态,但是保留之前的被包装的任务。
- valid():检查packaged_task对象是否具有共享状态。
设置异步结果
std::promise用来实现线程间的异步通信。promise提供了一个承诺(promise),表示在某个时间点一定会有一个值或一个异常被设置。
promise在一个线程中设置一个值,而另一个线程中可以通过std::future来获取这个值。通常的做法是:
- 创建一个promise对象。
- 通过promise对象获取一个future对象。
- 在一个线程中将值或异常设置到promise对象中
- 在另外一个线程通过future对象来获取值或异常。
std::promise对象只能使用一次。
构造函数
- 只有一个无参默认构造函数。
- 支持移动构造,不支持拷贝构造。
// 承诺提供一个int类型的值
std::promise<int> int_promise;
// 不返回任何值,只表示操作完成
std::promise<void> void_promise;
成员函数
get_future:返回与之关联的 future 对象,只能调用一次。
set_value:原子的设置值,并使 future 进入就绪状态。
set_value_at_thread_exit:原子的设置值,只有在调用该方法的线程结束并且所有线程局部对象被销毁后,future 才会进入就绪状态。
set_exception:设置异常,并使 future 进入就绪状态。
set_exception_at_thread_exit:设置异常,只有在调用该方法的线程结束并且所有线程局部对象被销毁后,future 才会进入就绪状态。