0%

C++ Future机制

Future机制

CPP Future机制用于获取异步任务的异常和返回结果,尽管也可以通过传入的指针或引用返回结果,但其需要引入外部变量,较为麻烦,且难以捕获异常。

Future机制由promise和future配合实现异步返回,promise用于在异步任务中设置返回结果,future用于获取在原线程中获取异步任务返回结果,两者间通过共享状态(shared state)进行通信。三者关系如下图:

CPP中的promise和future

CPP提供了std::promise充当Future机制中的promise,std::futurestd::shared_future充当Future机制中的future,两者共同作用解决完成异步数据传输。

promise

CPP提供了std::promise充当Future机制中的promise,std::promise只能进行默认构造和移动,无法进行拷贝。

std::promise提供如下接口进行数据传输:

方法 含义
std::future<R> get_future(); 返回一个与promise关联相同共享状态的std::future对象
void set_value(const R& value);
void set_value(R&& value);
立即设置promise的共享状态的值
void set_exception( std::exception_ptr p ); 立即设置promise的共享状态捕获的异常
void set_value_at_thread_exit(const R &value);
void set_value_at_thread_exit(R &&value);
在线程结束后设置promise的共享状态的值
void set_exception_at_thread_exit(std::exception_ptr p); 在线程结束后设置promise的共享状态捕获的异常

在使用std::promise时有以下注意事项:

  1. std::promise是一次性的,一旦设置了相应的值,再次调用设值函数将会抛出异常。

  2. std::promise的任何方法都不会断开与共享状态的连接,因此可以多次调用get_future

  3. set_*set_*_at_thread_exit之间的区别在于,set_*将会使得与该对象内部的共享状态相关联的future将能够立即获得值;set_*_at_thread_exit将会使得与该对象内部的共享状态相关联的future将在异步线程退出后才获得值。

future

CPP提供了std::futurestd::shared_future充当Future机制中的future,两者提供了一些相同的接口用于实现异步线程数据传输:

方法 含义
T get();
T& get();
获取共享状态的值或捕获的异常。
若此时共享状态未被设置值或捕获异常则进行阻塞等待
bool valid() const noexcept; 当future绑定共享状态时返回true,否则返回false
void wait() const; 阻塞等待,直到共享状态被设置值或捕获异常
template <class Rep, class Period>
std::future_status wait_for(const std::chrono::duration<Rep,Period>& timeout_duration) const;
阻塞等待,直到共享状态被设置值或捕获异常,或者线程已被阻塞了timeout_duration时间
template <class Clock, class Duration>
std::future_status wait_until(const std::chrono::time_point<Clock,Duration>& timeout_time) const;
阻塞等待,直到共享状态被设置值或捕获异常,或者线程被阻塞直到timeout_time

尽管std::futurestd::shared_future提供了相似的异步线程数据传输接口,但二者具有以下区别:

std::future std::shared_future
只能进行默认构造或移动,不能进行拷贝 能进行默认构造、拷贝和移动
无法进行安全的线程共享 同一个实例无法安全的进行线程共享
拷贝生成的示例能够在多线程环境下安全的对共享状态的值进行赋值
调用get函数后将与相应的共享状态切断联系,因此只能调用一次 调用get函数后不与相应的共享状态切断联系,因此能够调用多次
能够通过std::shared_funtre<T> share() noexcept;转化为std::shared_future
调用该方法后将与相应的共享状态切断联系
能够接收std::funtre<T> &&进行构造
调用该方法后输入的std::funtre<T> &&将与相应的共享状态切断联系

对于wait_forwait_until,可以通过返回值判断其返回时的状态,其返回值可能为:

  1. std::future_status::ready:共享状态就绪。

  2. std::future_status::timeout:等待超时。

  3. std::future_status::deferred:共享状态关联的是一个deferred的函数,只有在调用get时才能赋值(详见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
36
37
#include <iostream>
#include <future>

void test_value() {
std::promise<int> pro;
std::future<int> fut = pro.get_future();
std::cout << fut.valid() << std::endl;
pro.set_value(1024);
std::cout << fut.valid() << std::endl;
std::cout << fut.get() << std::endl;
std::cout << fut.valid() << std::endl;
}

void test_execption() {
std::promise<int> pro;
std::future<int> fut = pro.get_future();
try {
throw std::runtime_error("Example");
} catch (...) {
try {
std::cout << "Catch" << std::endl;
pro.set_exception(std::current_exception());
} catch (...) {
std::cout << "Error" << std::endl;
}
} try {
std::cout << fut.get() << std::endl;
} catch (const std::exception &e) {
std::cout << "Throw: " << e.what() << '\n';
}
}

int main(int argc, char *argv[]) {
test_value();
test_execption();
return 0;
}

运行后打印:

1
2
3
4
5
6
1
1
1024
0
Catch
Throw: Example

CPP异步任务

为了便于创建和使用异步任务而无需手动管理promisefuture,CPP提供了std::packaged_taskstd::async用于构建异步任务。

packaged_task

CPP提供了std::packaged_task类用于方便的将已有函数打包为异步可调用对象,其提供了默认构造函数、基于仿函数的构造函数、移动构造函数和移动赋值函数来设置相应的值,并提供以下方法:

方法 含义
bool valid() const noexcept; 返回packaged_task内部是否有有效的仿函数
std::future get_future(); 返回一个与packaged_task关联的future对象
可以多次调用返回多个关联相同共享状态的future
void operator()(ArgTypes… args); 使用args作为参数调用packaged_task内的仿函数,并立即将调用结果赋值到共享状态中
void make_ready_at_thread_exit(ArgTypes… args); 使用args作为参数调用packaged_task内的仿函数,调用结果将在异步线程结束后赋值到共享状态中
void reset(); 重置内部状态,其所关联的future全部失效,但具有的仿函数不变

下面是一个简单的使用示例:

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

std::string func(int s) {
return std::to_string(s | INT32_MIN);
}

int main(int argc, char *argv[]) {
std::packaged_task<std::string(int)> ptask(func);
std::future<std::string> f = ptask.get_future();
std::thread th(std::move(ptask), INT32_MAX);
std::cout << f.get() << std::endl;
th.join();
return 0;
}

运行后将获得如下结果:

1
-1

async

CPP提供了std::async函数方便的创建并运行异步任务,其函数签名如下:

1
2
3
4
5
template <class F, class ... Args>
std::future<...> async(F&& f, Args&& ... args);

template <class F, class ... Args>
std::future<...> async(std::launch policy, F&& f, Args&& ... args);

其中,不带policy参数的版本相当于向带参数版本中传入policy参数std::launch::async | std::launch::deferred

该函数能够创建一个异步任务,其有两种执行方式:

  1. 异步调用,在另一个线程(新线程或线程池中的线程)中运行异步任务,并设置共享状态传递执行结果。

  2. 延迟调用,当future调用waitget时在被阻塞线程运行异步任务并通过设置共享状态传递执行结果,调用wait_forwait_until时返回std::future_status::deferred

可以通过policy参数设置开启的异步任务的执行方式:

  1. 同时设置std::launch::asyncstd::launch::deferred:由标准库和当前系统状态进行决定,一般来说,当系统线程数量达到上限或系统线程超发(就绪线程数大于CPU并发线程数)时会采用延迟调用,否则会采用异步调用。

  2. 仅设置std::launch::async:总是进行异步调用。

  3. 仅设置std::launch::deferred:总是进行延迟调用。

  4. 未设置std::launch::asyncstd::launch::deferred:行为未定义。