在前面介绍了启动线程,以及多线程下如何保证共享资源的竞争访问、线程同步这些。但是thread类无法访问从线程直接返回的值,如果要想获取线程的的执行结果,一般都是依靠全局或static变量,或是以实参传递的变量,然后结合互斥锁、条件变量,等待的线程去查验所等待的条件。假如某个线程按计划只等待一次,只要条件成立一次,它就不再理会条件变量了,条件变量不一定就是这种同步模式的最佳选择, 如果我们等待的条件是判定某份数据是否可用,C++ 标准库提供的std::future类模板更适合这种场景。
C++ 标准库提供了std::future类模板来获取异步任务(即在单独的线程中启动的函数)的返回结果,并捕捉其所抛出的异常,这种获取结果的方式是异步的。如果线程需要等待某个特定的一次性事件发生,则会以恰当的方式取得一个future,它代表目标事件;接着这个该线程可以一边执行任务,一边在future上等待;同时,它以短暂的间隔反复查验目标事件是否已经发生。这个线程也可以转换运行模式,先不等目标事件发生,直接暂缓当前任务,而切换到别饿任务,到了必要时,才回头等待futere准备就绪。future可能与数据关联,也可能未关联。一旦目标事件发生,其future即进入就绪状态,无法重置 。
C++标准库使用std::future为一次性事件建模,如果一个事件需要等待特定的一次性事件,那么这线程可以获取一个future对象来代表这个事件。异步调用往往不知道何时返回,但是如果异步调用的过程需要同步,或者说后一个异步调用需要使用前一个异步调用的结果。这个时候就要用到future。线程可以周期性的在这个future上等待一小段时间,检查future是否已经ready,如果没有,该线程可以先去做另一个任务,一旦future就绪,该future就无法复位(无法再次使用这个future等待这个事件),所以future代表的是一次性事件。
std::future是一个类模板(class template),其对象存储未来的值,从一个异步调用的角度来说,future更像是执行函数的返回值,其模板参数就是期待返回的类型。
template
class future
{
public:
future() noexcept;
future(future&&) noexcept;
future& operator=(future&&) noexcept;
~future();
future(future const&) = delete;
future& operator=(future const&) = delete;
bool valid() const noexcept;
ResultType get();
shared_future share();
void wait();
template
future_status wait_for(
std::chrono::duration const& relative_time);
template
future_status wait_until(
std::chrono::time_point const& absolute_time);
};
成员函数 | 说明 |
构造函数 | (1).不带参数的默认构造函数,此对象没有共享状态,因此它是无效的,但是可以通过移动赋值的方式将一个有效的future值赋值给它; (2).禁用拷贝构造; (3).支持移动构造 |
析构函数 | |
operator= | 移动future对象 (公开成员函数) (2).支持移动赋值:如果在调用之前,此对象是有效的(即它已经访问共享状态),则将其与先前已关联的共享状态解除关联。如果它是与先前共享状态关联的唯一对象,则先前的共享状态也会被销毁 |
share | 从 *this 转移共享状态给 shared_future 并返回它 (公开成员函数) |
get() | 返回结果 (公开成员函数) (1).当共享状态就绪时,返回存储在共享状态中的值(或抛出异常)。 (2).如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。 (3).当共享状态就绪后,则该函数将取消阻塞并返回(或抛出)释放其共享状态,这使得future对象不再有效,因此对于每一个future共享状态,该函数最多应被调用一次。(4).std::future (5).共享状态是作为原子操作(atomic operation)被访问 |
valid() | 检查 future 是否拥有共享状态(公开成员函数) |
wait() | 等待结果变得可用 (1).等待共享状态就绪。 (2).如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。 (3).当共享状态就绪后,则该函数将取消阻塞并void返回 |
wait_for() | 等待结果,如果在指定的超时间隔后仍然无法得到结果,则返回。 |
wait_until() | 等待结果,如果在已经到达指定的时间点时仍然无法得到结果,则返回。 |
在库的头文件中声明了两种future,唯一future(std::future<>)和共享future(std::shared_future<>)这两个是参照std::unique_ptr和std::shared_ptr设立的,
仅有一个指向其关联事件的实例,
共享future(std::shared_future<>)可以有多个实例指向同一个关联事件,当事件就绪时,所有指向同一事件的std::shared_future实例会变成就绪。
总之,类模板 std::future 提供访问异步操作结果的机制:
1. 提供一个 std::future 对象给异步操作的创建者,一个有效的std::future对象通常是由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,通常是:
由 std::future 默认构造函数创建的 future 对象不是有效的(除非当前非有效的 future 对象被 move 赋值另一个有效的 future 对象)
2. Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)。
因为std::thread没有提供直接回传结果的方法,函数模板std::async()应运而生。只要我们并不着急需要线程运算的结果,就可以用std::async()按异步方式启动任务。我们从std::async()函数处获得std::future对象(而非thread对象),运行的函数一旦完成,其返回值就有该对象最后持有。若要用到这个值,只需要在future对象上调用get(),当前线程就会阻塞,以便future准备完毕并返回这个值.
template
future
参数 | 描述 |
fn | 任务函数(仿函数、lambda表达式、类成员函数、普通函数……) 1.如果是要异步运行某个类的某个成员函数 任务函数这个参数应该是一个函数指针,指向该类的目标成员函数; 任务函数这个参数需要给出相应的对象,以在它之上调用成员函数(这个参数可以是指向对象的指针,或对象本身,或ref包装的对象) 余下的async()的参数会传递给成员函数,用作成员函数的参数 2. 如果运行的是普通函数 第一个参数是指定任务函数(或目标可调用对象),其参数取自async()里余下的参数。 |
policy | 决定异步执行,还是同步执行任务
|
下面演示了用launch::async和launch::deferred两种不同的policy执行任务函数的差别,任务函数都是返回tid。从运行结果看得出来,使用async policy的方式是另起了一个thread运行的任务函数,因为其tid和调用线程的tid不同;相反,使用deferred policy的调用线程的tid和运行任务函数的tid是相等的,说明是在调用线程里调用的任务函数,而没有单独起一个线程去做。
#include
#include
#include
using namespace std;
thread::id test_async() {
this_thread::sleep_for(chrono::milliseconds(500));
return this_thread::get_id();
}
thread::id test_async_deferred() {
this_thread::sleep_for(chrono::milliseconds(500));
return this_thread::get_id();
}
int main() {
future ans = std::async(launch::async, test_async); //另起一个线程去运行test_async
future ans_def = std::async(launch::deferred,test_async_deferred); //还没有运行test_async_deferred
cout << "main thread id = " << this_thread::get_id() << endl;
cout << "test_async thread id = " << ans.get() << endl;//如果test_async这时还未运行完,程序会阻塞在这里,直到test_async运行结束返回
cout << "test_async_deferred thread id = = " << ans_def.get() << endl;//这时候才去调用test_async_deferred,程序会阻塞在这里,直到test_async_deferred运行返回
return 0;
}
package_task<>是一个类模板,packaged_task类把一个可调用目标(函数、lambda表达式、bind表达式、函数对象)包装成一个对象,以便它可以被异步调用。packaged_task它连结了future对象与函数,package_task<>对象在执行任务时,会调用关联的函数,把返回值保存为future的内部数据,并令future准备就绪。
package_task<>其模板参数是函数签名:比如,void()表示一个函数,不接收参数,也不接收返回值;int(string&,double*)代表某函数,它接收两个参数并返回int值。假设我们要构建packaged_task<>实例,那么,由于模板参数先行指定了函数签名,因此传入的函数必须与之相符。即它应该接收指定类型的参数,返回值也必须可以转换为指定类型。
packaged_task对象是可调用对象,我们可以直接调用,还可以将其包装在function对象内,当作线程函数传递给thread对象,也可以传递给需要可调用对象的函数。如果packaged_task作为函数对象而被调用,它就会通过函数调用操作符接收参数,并将其进一步传递给包装在内的任务函数,由其异步运行得出结果,并将其结果保存到future对象内部,再通过get_future()获取此对象。所以,为了在未来的适当时刻执行某项任务,我们可以将其包装在packaged_task对象内,取得对应的future之后,才把该对象传递给其他线程,由它触发任务执行。等到需要用到使用结果时,我们静候future准备就绪即可。
#include
#include
#include
using namespace std;
int add(int a,int b) {
cout <<"sub thread id: " << this_thread::get_id() << endl;
return a + b;
}
int main() {
cout << "main thread id: " << this_thread::get_id() << endl;
packaged_task task_add(add);
future res = task_add.get_future();
//另起线程调用
thread th(move(task_add),100,500);
//也可以直接调用,和调用线程处于同一线程
//task_add(100,500);
cout << "res = " << res.get() << endl;
th.join();
return 0;
}
有些任务无法以简单的函数调用表达出来,还有一些任务的执行结果可能来自多个部分的代码。promise
如下这段程序,创建一个 promise 对象,然后通过 promise 对象获取一个 future 对象,promise 在一个线程中设置了一个值,将 future 对象传递到另一个线程中去,而另一个线程中可以通过 std::future 来访问这个值(或者异常)。
#include
#include
#include
using namespace std;
void add(int a, int b, promise& p) {
p.set_value(a + b);
}
void useResult(future& f) {
int val = f.get();//阻塞函数,直到收到相关联的std::promise对象传入的数据
cout << "useResult: val = " << val << endl;
}
int main() {
promise prom;
future fut = prom.get_future();
thread th(add,100,90,ref(prom));
thread th2(useResult,ref(fut));
th.join();
th2.join();
return 0;
}
promise 提供了一个承诺(promise),表示在某个时间点一定会有一个值或一个异常会被设置。