• 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类模板

    1. template<typename ResultType>
    2. class future
    3. {
    4. public:
    5. future() noexcept;
    6. future(future&&) noexcept;
    7. future& operator=(future&&) noexcept;
    8. ~future();
    9. future(future const&) = delete;
    10. future& operator=(future const&) = delete;
    11. bool valid() const noexcept;
    12. ResultType get();
    13. shared_future share();
    14. void wait();
    15. template<typename Rep,typename Period>
    16. future_status wait_for(
    17. std::chrono::duration const& relative_time);
    18. template<typename Clock,typename Duration>
    19. future_status wait_until(
    20. std::chrono::time_point const& absolute_time);
    21. };
    成员函数说明
    构造函数

    (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是相等的,说明是在调用线程里调用的任务函数,而没有单独起一个线程去做。

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. thread::id test_async() {
    6. this_thread::sleep_for(chrono::milliseconds(500));
    7. return this_thread::get_id();
    8. }
    9. thread::id test_async_deferred() {
    10. this_thread::sleep_for(chrono::milliseconds(500));
    11. return this_thread::get_id();
    12. }
    13. int main() {
    14. future ans = std::async(launch::async, test_async); //另起一个线程去运行test_async
    15. future ans_def = std::async(launch::deferred,test_async_deferred); //还没有运行test_async_deferred
    16. cout << "main thread id = " << this_thread::get_id() << endl;
    17. cout << "test_async thread id = " << ans.get() << endl;//如果test_async这时还未运行完,程序会阻塞在这里,直到test_async运行结束返回
    18. cout << "test_async_deferred thread id = = " << ans_def.get() << endl;//这时候才去调用test_async_deferred,程序会阻塞在这里,直到test_async_deferred运行返回
    19. return 0;
    20. }

     

    c72945cb8653432b93b3a36b5ddf4d29.png

     

    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准备就绪即可。

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. int add(int a,int b) {
    6. cout <<"sub thread id: " << this_thread::get_id() << endl;
    7. return a + b;
    8. }
    9. int main() {
    10. cout << "main thread id: " << this_thread::get_id() << endl;
    11. packaged_task<int(int, int)> task_add(add);
    12. future<int> res = task_add.get_future();
    13. //另起线程调用
    14. thread th(move(task_add),100,500);
    15. //也可以直接调用,和调用线程处于同一线程
    16. //task_add(100,500);
    17. cout << "res = " << res.get() << endl;
    18. th.join();
    19. return 0;
    20. }

    14a1d39ba2ab481fa4c277f47dce5b8b.png

     

    std::promise

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

    如下这段程序,创建一个 promise 对象,然后通过 promise 对象获取一个 future 对象,promise 在一个线程中设置了一个值,将 future 对象传递到另一个线程中去,而另一个线程中可以通过 std::future 来访问这个值(或者异常)。

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void add(int a, int b, promise<int>& p) {
    6. p.set_value(a + b);
    7. }
    8. void useResult(future<int>& f) {
    9. int val = f.get();//阻塞函数,直到收到相关联的std::promise对象传入的数据
    10. cout << "useResult: val = " << val << endl;
    11. }
    12. int main() {
    13. promise<int> prom;
    14. future<int> fut = prom.get_future();
    15. thread th(add,100,90,ref(prom));
    16. thread th2(useResult,ref(fut));
    17. th.join();
    18. th2.join();
    19. return 0;
    20. }

           promise 提供了一个承诺(promise),表示在某个时间点一定会有一个值或一个异常会被设置。

     

     

  • 相关阅读:
    程序员保密协议公司之间
    【深度梯度投影网络:遥感图像】
    做为测试人,开发BUG频出,我该最怎样避免线上事故......
    HIK录像机GB28181对接相机不在线问题随笔
    docker 基本用法-操作镜像
    Android:Handler
    【HDU No. 2874】 城市之间的联系 Connections between cities
    【算法学习】-【双指针】-【复写零】
    华为云云耀云服务器L实例使用教学:安装jdk和配置环境变量
    17、学习MySQL 事务
  • 原文地址:https://blog.csdn.net/weixin_42463482/article/details/132636875