首页 > 编程语言 >C++中promise和future初认识

C++中promise和future初认识

时间:2024-02-04 19:44:58浏览次数:29  
标签:std set get value future promise C++

future/promise

future提供了一个基于数据(future模板类型)的异步概念:对于一个类型T,可以在以后通过get接口获得这个类型T的变量。或者打个不太恰当的比方,当你获得一个future对象时,就获得了一个消费券(consumer):拿着这张券可以兑换(get)一个T类型的结果(如果数据未就绪的话会阻塞等待)。至于这个类型是从哪里来的future对象本身并不关系,它是future提供者的职责。

事实上,从future的文档(为了方便阅读单独拷贝一份)就可以看到,除了promise之外,还有std::async、std::packaged_task也可以提供future。promise只是抽象了一种数据结构(但是没有提供具体如何异步),这种数据结构支持set_value和future::get的时序/等待/状态/异常处理等常用逻辑。当数据就绪之后,直接通过promise::set_value生产数据即可。至于这个set_value从哪里来,promise本身也不关系(它只是一个数据结构),需要具体生产者负责。

#include <future>
#include <iostream>
#include <thread>
 
int main()
{
    // future from a packaged_task
    std::packaged_task<int()> task([]{ return 7; }); // wrap the function
    std::future<int> f1 = task.get_future(); // get a future
    std::thread t(std::move(task)); // launch on a thread
 
    // future from an async()
    std::future<int> f2 = std::async(std::launch::async, []{ return 8; });
 
    // future from a promise
    std::promise<int> p;
    std::future<int> f3 = p.get_future();
    std::thread([&p]{ p.set_value_at_thread_exit(9); }).detach();
 
    std::cout << "Waiting..." << std::flush;
    f1.wait();
    f2.wait();
    f3.wait();
    std::cout << "Done!\nResults are: "
              << f1.get() << ' ' << f2.get() << ' ' << f3.get() << '\n';
    t.join();
}

再笼统的说:promise和future大致相当于一个管道(Unix中pipe)文件的两个两端,分别是输入和读取数据的两端,具体数据如何产生,消费的数据如何处理,这种机制本身并不关心。

解耦

  • 使用场景

future并不是一定依赖于promise的,其它的一些场景下也可以生成future。

  • producer/consumer

promise中有set_value接口,而future中有get接口;这个描述还有一个对应的反面:promise没有get(value)接口,future没有set_value接口。

从逻辑上拆分之后,不通过类型的职责更加单一,也更易于理解使用场景。

  • 生存期

当future生成(例如promise::get_future)之后,future可以从它的原始“生产地”中完全脱离出来。例如,如果promise在set_value之后对象本身析构。

问题

如果promise设置了值(set_value)之后,在future执行get之前析构,此时future的get还有效吗?

SO的答案说明:如果在销毁之前给promise提供了value,那么对应的std::future可以毫无压力的获得设置值。

If you provide a value to the std::promise before destroying it, the associated std::future will be able to retrieve it just fine. It does not depend on std::promise still existing. If you failed to provide a value to std::promise before destroying it, trying to get a result from the std::future will throw std::future_error but it is also well defined.

查看源代码中promise的析构函数,在析构的时候,判断如果_M_future非空,并且_M_future指向内容有多个shared_ptr引用,那么会设置_M_future为_M_break_promise。这样的话,通过future来get的时候,应该会抛出异常才对。到底是哪里出了问题呢?

      ~promise()
      {
        if (static_cast<bool>(_M_future) && !_M_future.unique())
          _M_future->_M_break_promise(std::move(_M_storage));
      }

测试

从测试来看,在promise析构之后,get_futre().get()的确可以毫无障碍的获得promise析构前设置的value。

tsecer@harry: cat  dest_promise.cpp   
#include <future>
#include <stdio.h>

int main(int argc, const char *argv[])
{
    std::promise<int> *pp = new std::promise<int>;
    std::future<int>  of  = pp->get_future();
    pp->set_value(1234);
    delete pp;
    printf("future.get is %d\n", of.get());
    return 0;
}

tsecer@harry: g++ dest_promise.cpp -g -lpthread
tsecer@harry: ./a.out 
future.get is 1234
tsecer@harry: 

原因

由于问题明显而必现,在调试的帮助下很容易知道问题的原因:在执行promise析构函数时,_M_storage内容已经为空,而在_M_break_promise函数中,会判断指针是否为空,在指针为空的情况下并不会设置broken_promise。

      // Abandon this shared state.
      void
      _M_break_promise(_Ptr_type __res)
      {
	if (static_cast<bool>(__res))
	  {
	    __res->_M_error =
	      make_exception_ptr(future_error(future_errc::broken_promise));
	    // This function is only called when the last asynchronous result
	    // provider is abandoning this shared state, so noone can be
	    // trying to make the shared state ready at the same time, and
	    // we can access _M_result directly instead of through call_once.
	    _M_result.swap(__res);
	    // Use release MO to synchronize with observers of the ready state.
	    _M_status._M_store_notify_all(_Status::__ready,
					  memory_order_release);
	  }
      }

那么_M_storage是何时清空的呢?

由于demo代码比较简单,可以跟踪到是在set_value的时候,经过多层调用,最后会执行到这里的operator()() 函数,其中有一个std::move(_M_promise->_M_storage),在_M_storage是一个unique_ptr的情况下,move操作会导致复制的源内容被清空。

      // set void
      template<typename _Res>
	struct _Setter<_Res, void>
	{
	  static_assert(is_void<_Res>::value, "Only used for promise<void>");

	  typename promise<_Res>::_Ptr_type operator()() const
	  { return std::move(_M_promise->_M_storage); }

	  promise<_Res>*    _M_promise;
	};

unique_ptr,明确说明了如果是一个右值引用,这种构造函数将会转移所有权(by transferring ownership)

  1. Constructs a unique_ptr by transferring ownership from u to *this and stores the null pointer in u. This constructor only participates in overload resolution if std::is_move_constructible::value is true. If Deleter is not a reference type, requires that it is nothrow-MoveConstructible (if Deleter is a reference, get_deleter() and u.get_deleter() after move construction reference the same value).

回顾promise的结构及get_future

  • 结构

promise包括两个结构,一个是通过shared_ptr指向的future结构(_M_future),一个是通过unique_ptr指向的类型(promise参数中的类型)数据结构(_M_storage字段)。

  /// Primary template for promise
  template<typename _Res>
    class promise
    {
    ///...
          typedef __future_base::_Ptr<_Res_type>	_Ptr_type;
    ///...
          shared_ptr<_State>                        _M_future;
      _Ptr_type                                 _M_storage;
    ///...
    
    ///...
    }

其中的_Ptr_type是一个unique_ptr类型

  /// Base class and enclosing scope.
  struct __future_base
  {
  
    /// A unique_ptr for result objects.
    template<typename _Res>
      using _Ptr = unique_ptr<_Res, _Result_base::_Deleter>;
}

future内容如下

    // Base class for various types of shared state created by an
    // asynchronous provider (such as a std::promise) and shared with one
    // or more associated futures.
    class _State_baseV2
    {
      typedef _Ptr<_Result_base> _Ptr_type;

      enum _Status : unsigned {
	__not_ready,
	__ready
      };

      _Ptr_type			_M_result;
      __atomic_futex_unsigned<>	_M_status;
      atomic_flag         	_M_retrieved = ATOMIC_FLAG_INIT;
      once_flag			_M_once;

可以看到其中除了同步、状态之外,还有一个指向类型的_M_result字段,它在执行_M_future初始化的时候并没有和_M_storage关联起来。

  • 分配

在promise的缺省构造函数中会为分配对应的存储空间。

      promise()
      : _M_future(std::make_shared<_State>()),
	_M_storage(new _Res_type())
      { }
  • 所有权

正如前面所说,当自行set_value时,会设置std::move(_M_promise->_M_storage)将_M_storage指向空间的所有权转让出去,对于set_value来说,就是转让给了_M_future结构的_Ptr_type字段。

当通过promise::get_future获得future时,会获得一个指向_State的指针。由于这个字段是shared_ptr,所以get_future之后返回的future对象和这个promise(以shared_ptr)形式共享这个future成员。

总起来说,它们的生命期维护关系为: _M_future是一个promise和future共同维护的shared_ptr,两个结构中的任意一个销毁都不会导致指向内容释放;结构中的_Ptr_type是一个unique_ptr,当_M_future析构的时候才会释放

如何出现析构中的Broken promise

在确定了_M_storage是由于set_value而清空的之后,构造Broken promise的方法就很简单了:就是不调用set_value即可。

tsecer@harry: cat no_set_value_dest_promise.cpp
#include <future>
#include <stdio.h>

int main(int argc, const char *argv[])
{
    std::promise<int> *pp = new std::promise<int>;
    std::future<int>  of  = pp->get_future();
    //pp->set_value(1234);
    delete pp;
    printf("future.get is %d\n", of.get());
    return 0;
}

tsecer@harry: g++ -g no_set_value_dest_promise.cpp -lpthread
tsecer@harry: ./a.out 
terminate called after throwing an instance of 'std::future_error'
  what():  std::future_error: Broken promise
已放弃 (core dumped)
tsecer@harry: 

标签:std,set,get,value,future,promise,C++
From: https://www.cnblogs.com/tsecer/p/18006889

相关文章

  • C++多线程 第三章 在线程间共享数据
    第三章在线程间共享数据共享数据基本问题如果所有共享数据都只读,那就没有问题.不变量(invariants):对特定数据结构总为真的语句.例如:"该变量表示线程数量."修改线程之间共享数据的一个常见潜在问题就是破坏不变量.竞争条件(racecondition):线程竞争执行各自的操作,导......
  • C++之INI配置文件读写/注释库 inicpp 介绍【简单易用-包含inicpp.hpp头文件即可】
    一个头文件(header-file-only)搞定INI文件读写、甚至进行注释。跨平台,并且用法极其简单。MITlicense,从此配置INI文件就像喝水。【注:对您有帮助的话,Star或Issues为项目维护提供动力,感谢。】-byofficalofJN-inicppproject.一、库下载https://github.com/dujingning/inicpp......
  • 14. C++函数的编译
    C++函数的编译C++和C语言的编译方式不同。C语言中的函数在编译时名字不变,或者只是简单的加一个下划线_(不同的编译器有不同的实现),例如,func()编译后为func()或_func()。而C++中的函数在编译时会根据它所在的命名空间、它所属的类、以及它的参数列表(也叫参数签名)等信息进行重新......
  • 创建大量栅格文件并分别写入像元数据:C++ GDAL代码实现
      本文介绍基于C++语言GDAL库,批量创建大量栅格遥感影像文件,并将数据批量写入其中的方法。  首先,我们来明确一下本文所需实现的需求。已知我们对大量遥感影像进行了批量读取与数据处理操作——具体过程可以参考文章C++GDAL提取多时相遥感影像中像素随时间变化的数值数组;而随......
  • Promise, async, await实现异步编程,代码详解
    写在开头一点题外话其实最近在不断的更新Java的知识,从基础到进阶,以及计算机基础、网络、WEB、数据库、数据结构、Linux、分布式等等内容,预期写成一个既可以学习提升又可以面试找工作的《Java成长之路》!算是对自己学习的总结(笔记),也算是一种程序猿的记忆,现在大环境那么差,万一哪天......
  • A Knight's JourneyC++
    题目看半天看不懂。題目把我恶心坏了。看网上说按字典顺序输出,到底是什么意思半天没搞懂。#include<iostream>#include<string>usingnamespacestd;intd[8][2]={{-1,-2},{1,-2},{-2,-1},{2,-1},{-2,1},{2,1},{-1,2},{1,2}};intvisit[8][8]={0};boolDFS(i......
  • CLion 2023: 一款专注于性能和效率的C/C++ IDE mac/win版
    JetBrainsCLion2023是一款专为C和C++开发人员打造的强大集成开发环境。这个版本致力于提供卓越的性能、强大的功能和一流的智能代码编辑支持,帮助您更高效地开发高质量的C和C++应用程序。→→↓↓载CLion2023mac+win版首先,CLion2023提供了对最新C和C++标准的全面支持。无论......
  • c++20模块化编程与传统区别
    传统:main.cpp+a.cpp(存放定义)+a.h(存放声明)c++20:main.cpp+a.cppm(存放定义,在定义前面写export即可)模块化编程好处:不再需要修改了函数到对应修改声明,两头跑编译更快,模块只在修改后才重新编译模块化编程举例://my_module.cppmimport<iostream>;exportm......
  • Find The MultipleC++
    这题就是找N的倍数m,M要求是由1和0组成且非0。可以用图来看,从1出发临边是1和0,然后广度遍历,第一个能能整除N的数输出就行。#include<iostream>#include<queue>usingnamespacestd;intmain(){intn=-1;while(cin>>n){if(n==0)break;longlon......
  • c++加速cin和关闭同步流
    signedmain(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);intT=1;//cin>>T;while(T--)solve();return0;}一·ios::sync_with_stdio(false);01"c++是否兼容stdio(c)"的开关函数02默认参数为true:将输出流绑到一起保证......