C++ 多线程(future篇)

引言

       在前面介绍了启动线程,以及多线程下如何保证共享资源的竞争访问、线程同步这些。但是thread类无法访问从线程直接返回的值,如果要想获取线程的的执行结果,一般都是依靠全局或static变量,或是以实参传递的变量,然后结合互斥锁、条件变量,等待的线程去查验所等待的条件。假如某个线程按计划只等待一次,只要条件成立一次,它就不再理会条件变量了,条件变量不一定就是这种同步模式的最佳选择, 如果我们等待的条件是判定某份数据是否可用,C++ 标准库提供的std::future类模板更适合这种场景。

         C++ 标准库提供了std::future类模板来获取异步任务(即在单独的线程中启动的函数)的返回结果,并捕捉其所抛出的异常,这种获取结果的方式是异步的。如果线程需要等待某个特定的一次性事件发生,则会以恰当的方式取得一个future,它代表目标事件;接着这个该线程可以一边执行任务,一边在future上等待;同时,它以短暂的间隔反复查验目标事件是否已经发生。这个线程也可以转换运行模式,先不等目标事件发生,直接暂缓当前任务,而切换到别饿任务,到了必要时,才回头等待futere准备就绪。future可能与数据关联,也可能未关联。一旦目标事件发生,其future即进入就绪状态,无法重置 。    

std::future

      C++标准库使用std::future为一次性事件建模,如果一个事件需要等待特定的一次性事件,那么这线程可以获取一个future对象来代表这个事件。异步调用往往不知道何时返回,但是如果异步调用的过程需要同步,或者说后一个异步调用需要使用前一个异步调用的结果。这个时候就要用到future。线程可以周期性的在这个future上等待一小段时间,检查future是否已经ready,如果没有,该线程可以先去做另一个任务,一旦future就绪,该future就无法复位(无法再次使用这个future等待这个事件),所以future代表的是一次性事件。       

         std::future是一个类模板(class template),其对象存储未来的值,从一个异步调用的角度来说,future更像是执行函数的返回值,其模板参数就是期待返回的类型。



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对象 (公开成员函数)
(1).禁用拷贝赋值。

(2).支持移动赋值:如果在调用之前,此对象是有效的(即它已经访问共享状态),则将其与先前已关联的共享状态解除关联。如果它是与先前共享状态关联的唯一对象,则先前的共享状态也会被销毁

share 从 *this 转移共享状态给 shared_future 并返回它 (公开成员函数)
get()

返回结果 (公开成员函数)

(1).当共享状态就绪时,返回存储在共享状态中的值(或抛出异常)。

(2).如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。

(3).当共享状态就绪后,则该函数将取消阻塞并返回(或抛出)释放其共享状态,这使得future对象不再有效,因此对于每一个future共享状态,该函数最多应被调用一次。(4).std::future::get()不返回任何值,但仍等待共享状态就绪并释放它。

(5).共享状态是作为原子操作(atomic operation)被访问

valid() 检查 future 是否拥有共享状态(公开成员函数)
wait()

等待结果变得可用

(1).等待共享状态就绪。

(2).如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。

(3).当共享状态就绪后,则该函数将取消阻塞并void返回

wait_for() 等待结果,如果在指定的超时间隔后仍然无法得到结果,则返回。
wait_until() 等待结果,如果在已经到达指定的时间点时仍然无法得到结果,则返回。 

std::future的类型

 在库的头文件中声明了两种future,唯一future(std::future<>)和共享future(std::shared_future<>)这两个是参照std::unique_ptr和std::shared_ptr设立的,

  • 唯一future(std::future<>)

仅有一个指向其关联事件的实例,

  • 共享future(std::shared_future<>)

共享future(std::shared_future<>)可以有多个实例指向同一个关联事件,当事件就绪时,所有指向同一事件的std::shared_future实例会变成就绪。

总之,类模板 std::future 提供访问异步操作结果的机制:

1. 提供一个 std::future 对象给异步操作的创建者,一个有效的std::future对象通常是由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,通常是:

  • std::async() 函数
  • std::promise::get_future(),get_future() 为 promise 类的成员函数
  • std::packaged_task::get_future(),此时 get_future()为 packaged_task的成员函数

      由 std::future 默认构造函数创建的 future 对象不是有效的(除非当前非有效的 future 对象被 move 赋值另一个有效的 future 对象)

2. Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)。

std::async()

        因为std::thread没有提供直接回传结果的方法,函数模板std::async()应运而生。只要我们并不着急需要线程运算的结果,就可以用std::async()按异步方式启动任务。我们从std::async()函数处获得std::future对象(而非thread对象),运行的函数一旦完成,其返回值就有该对象最后持有。若要用到这个值,只需要在future对象上调用get(),当前线程就会阻塞,以便future准备完毕并返回这个值.

std::async函数原型

template

future::type> async(launch policy, Fn&& fn, Args&&...args);

参数 描述
fn

任务函数(仿函数、lambda表达式、类成员函数、普通函数……)

1.如果是要异步运行某个类的某个成员函数

任务函数这个参数应该是一个函数指针,指向该类的目标成员函数;

任务函数这个参数需要给出相应的对象,以在它之上调用成员函数(这个参数可以是指向对象的指针,或对象本身,或ref包装的对象)

余下的async()的参数会传递给成员函数,用作成员函数的参数

2. 如果运行的是普通函数

第一个参数是指定任务函数(或目标可调用对象),其参数取自async()里余下的参数。

policy

决定异步执行,还是同步执行任务

  1. std::launch::async 异步执行传递的任务函数,必须另外开启专属的线程去运行任务函数。
  2. std::launch::deferred 同步执行传递的任务函数,在当前线程上延后调用任务函数,等到了在future上调用了wait()或get(),任务函数才会执行,即函数调用被延迟。
  3. std::launch::async | std::launch::deferred 可以异步或是同步,取决于操作系统,我们无法控制;
  4. 如果我们不指定策略,则相当于3。

下面演示了用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;
}

std::packaged_task

         package_task<>是一个类模板,packaged_task类把一个可调用目标(函数、lambda表达式、bind表达式、函数对象)包装成一个对象,以便它可以被异步调用。packaged_task它连结了future对象与函数,package_task<>对象在执行任务时,会调用关联的函数,把返回值保存为future的内部数据,并令future准备就绪。

         package_task<>其模板参数是函数签名:比如,void()表示一个函数,不接收参数,也不接收返回值;int(string&,double*)代表某函数,它接收两个参数并返回int值。假设我们要构建packaged_task<>实例,那么,由于模板参数先行指定了函数签名,因此传入的函数必须与之相符。即它应该接收指定类型的参数,返回值也必须可以转换为指定类型。

  1. 类模板packaged_task<>具有成员函数get_future,它返回future<>实例,该future的特化类型取决于函数签名所指定的返回值
  2. 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;
}

std::promise

有些任务无法以简单的函数调用表达出来,还有一些任务的执行结果可能来自多个部分的代码。promise给出了一种异步求值的方法,某个future对象与结果关联,能延后读出需要求取的值。配对的promise和future可实现下面的工作机制:等待数据的线程在future上阻塞,而提供数据的线程利用相配的promise设定关联的值,使future准备就绪。 

如下这段程序,创建一个 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),表示在某个时间点一定会有一个值或一个异常会被设置。

你可能感兴趣的:(c++,jvm)