在编程实践中,我们常常需要使用异步调用。通过异步调用,我们可以将一些耗时、阻塞的任务交给其他线程来执行,从而保证当前线程的快速响应能力。还有一些场景可以通过将一个任务,分成多个部分然后将这部分交给多个线程来进行并发执行,从而完成任务的快速计算执行,提高应用性能。这里就需要用到异步调用的概念
异步调用,就是当前线程将一个任务交给另外一个线程来进行执行,当前线程会接着执行当前任务,不需要等待这个交付给其他线程执行的任务的结果,直到其在未来的某一个时刻需要使用这个任务执行结果数据的时候
链接异步操作和异步操作的管道
创建一个异步调用,然后其他线程执行这个异步调用,异步调用执行完成之后会返回一个结果,然后异步调用创建方在需要使用这个异步调用结果的时候,通过某种方式来获取这个异步调用结果。C++
针对上述需求,提供了一些类对象来完成上面的过程,本文中讨论的C++
提供的两种对象为std::future
和std::promise
,二者的关系对应如下
如上图所示,异步调用创建的时候,会返回一个std::future
对象实例给异步调用创建方。异步调用执行方持有std::promise
对象实例。双方持有的std::promise
对象实例和std::future
对象实例分别连接一个共享对象,这个共享对象在异步调用创建方和异步调用执行方之间构建了一个信息同步的通道(channel
),双方通过这个通道进行异步调用执行情况的信息交互
异步调用执行方访问这个通道是通过自身持有的std::promise
实例来向通道中写入值的,异步调用创建方是通过自身持有的std::future
对象实例来获取通道中的值的。当异步调用执行方完成异步调用的执行之后,会通过std::promise
对象实例向通道中写入异步调用执行的结果值。异步调用创建方通过自身持有的std::future
对象实例来获取异步调用结果
异步调用执行方通过std::promise
来兑现承诺(promise
,承诺调用完成之后在未来某一时刻交付结果),异步调用创建方通过std::future
来获取这个未来的值(future
,未来兑现的承诺对应的结果值)
示例
下面,我们通过一段程序示例,来看一下上面这个模型对应的程序,程序如下
上述代码行4~6
,创建了一个promise
对象,并且从该对象实例中获取到了用于获取承诺兑现的值的std::future
对象实例,这样就构建了一个异步调用创建方(发起方)和异步调用执行方之间用于传递异步调用结果的数据通道
在异步调用任务中,完成计算之后,通过std::promise::set_value
来进行承诺兑现,将异步调用的结果写入通道中(对应代码行13
)。异步调用创建方通过std::future
中的get
方法来获取异步调用的结果(对应代码行18
)
上面代码对应的终端输出如下
async result is 21
有一个细节值得注意,上面代码行12
中在异步调用任务执行的时候,可以通过sleep_for
将当前线程(异步任务执行的线程)休眠了500ms
。这里是为了让异步调用的结果在异步调用创建线程获取结果的代码调用(对应代码行18
)之后在进行承诺兑现(通过set_value
交付异步调用的结果)。也就是说程序执行到代码行18
的时候,异步调用的结果还没有设置到通道中。但是最终终端还是正常的打印了结果。这是因为当调用std::future::get
方法获取异步调用结果的时候,如果此时异步调用没有执行完成,即没有向通道内写入异步调用的结果(异步调用结果没有ready
),此时当前线程会阻塞直到异步调用结果计算完成并且设置
源码实现
通过上面的概述介绍以及示例,我们了解了future
和promise
这两个对象的作用和使用,下面就来看看对应的源码实现。首先,我们先看一下promise
的源码实现
promise的源码实现
首先,我们先看一下promise
的数据成员,代码截图如下。从下图我们可以看出,promise
的数据成员只有一个__state_
该数据成员对应的类型是__assoc_state<_RP>*
,是一个指针类型。__assoc_state
从类型的名上我们可以看出,该指针指向的是一个状态,该状态对象对应的就是promise
和future
之间的通道。_Rp
为异步操作返回值的类型,也就是说异步该状态对象内部保存并传递异步调用的返回值
下面,我们先看一下promise
的默认构造函数的代码。如下图所示,该构造函数比较简单,就是通过new
操作符构建了一个关联状态对象__assoc_state<_Rp>
,用这个关联对象的地址信息初始化内部的指针变量__state_
,完成promise
和通道channel
(即__assoc_state
实例)的链接
下面,我们看一下get_future
的源码实现,代码如下。首先在代码行5
中判断当前promise
中是否关联了关联状态对象实例(通过判断__state_
是否为空),如果未关联则抛出异常(对应代码行6
)。如果关联了关联状态对象,则通过该关联对象的地址信息来构建future
对象,返回一个链接该关联状态对象的future
对象实例
下面,我们看一下set_value
的源码实现,代码如下。有两个版本的set_value
(代码行3
和代码行14
)内部逻辑是一样的,区别在于前者是对应于左值版本,后者是对应于右值版本。下面我们以左值版本为例,代码行5~6
中先判断了一下当前promise
实例是否链接了关联对象实例,如果未链接关联对象实例则抛出异常。如果关联了关联对象实例,则调用关联对象实例的set_value
成员方法,将__r
实参传入,完成将异步调用结果写入关联状态对象中(即所链接的通道中)
下面,我们看一下future
析构函数的源码实现,代码如下。代码行4
如果当前promise
链接了关联状态对象,则需要在析构的时候处理关联状态对象的信息
- 代码行
6
:__state_->has_value()
用来判断当前关联状态对象中是否存有异步调用结果信息(即是否被设置值或者异常信息,异常信息后面讨论);__state_->use_count()>1
用来判断当前关联状态对象中是否有被其他对象关联(在此处为是否有其他future
对象关联);此处if
内的整体条件判断是用来判断当前关联状态对象中在是否被其他future
对象所关联的情况下,没有被设定异步调用的结果的相关信息,如果条件成立执行代码行7~9
的代码,这两行代码是抛出一个broken_promise
的异常,用来表征当前promise
是一个坏的(broken
)promise
对象(没有兑现承诺) - 代码行
10
:调用__state_->__release_shared()
来释放对关联状态对象的链接,其背后的逻辑类似于shared_ptr
中的引用计数,当所有链接关联状态对象的对象都释放链接的时候,该关联状态对象会进行自身资源的释放,占用内存的归还,这部分我们会在后面进行展开讨论
future的源码实现
惯例首先我们看一下future
的数据成员,代码截图如下所示。和promise
一样,其内部仅仅有一个成员变量__state_
,该成员变量是一个指针,用来保存其链接的状态对象的地址信息
通过promise
和future
类的内部成员,我们可以看出这两个类都是通过内部的__state_
指针完成和通道的关联,这里的通道对应的实现就是__assoc_state
这个模版类
下面我们看一下这个类对应的构造函数,代码截图如下图所示。该构造函数接受一个关联状态对象的地址信息,然后使用该地址信息来初始化内部的__state_
变量,完成当前future
到关联状态对象的链接
完成状态对象信息的链接之后,通过代码行5
,调用关联状态对象的__attach_future
成员函数,完成future
和关联状态对象的链接(attach
)。该成员函数的内部具体细节将在__assoc_state
的讨论中展开
下面,我们看一下future
对象的get
函数的代码实现,代码截图如下所示。
- 代码行
5
:创建一个名为__
的unique_ptr
的临时对象,并且将__state_
托管给该对象,该对象是一个临时对象,将会在get
函数运行结束的时候进行释放,当该对象释放的时候,将会对通过__release_shared_count
函数来对__state_
指向的关联状态对象进行相关的处理 - 代码行
6~7
:将当前__state_
的值赋值给临时变量__s
,将__state_
进行置空,完成future
对状态对象的链接的断开 - 代码行
8
:通过调用__s->move()
来完成对关联状态中异步调用结果的获取,获取完成之后将该值作为get
函数的返回值进行返回。同时__s->move()
函数不仅仅是单纯的获取异步调用结果,同时还会判断是否有异步调用的值,如果异步调用没有完成,则会阻塞在move
函数中,等待异步调用完成
下面我们看一下__release_shared_count
的代码定义,代码截图如下所示。__release_shared_count
是一个仿函数,其内部代码行3
的函数调用操作符的重载就是该仿函数的逻辑,对传入的关联状态对象调用__release_shared
,完成对于内部引用计数的递减,当引用计数递减到-1
的时候,会对关联状态对象进行资源释放
上面的函数调用重载函数的形参是__shared_count*
类型,之所以不是__assoc_state*
类型,是因为这里是一个多态,__assoc_state
是__shared_count
的派生类,这里将在后面讨论__assoc_state
的时候展开讨论
上面我们看到在调用get
方法之后,future
断开了和关联状态对象的链接,这说明future
对象只能调用一次get
方法来获取,如果多次调用,其内部__state_
将为空指针,则会因为对空指针调用move
方法,造成未定义行为
下面,我们来看一下析构函数的代码实现,代码截图如下所示。如下图所示,如果当前future
有链接状态关联对象,则调用其__release_shared
成员函数,从而对其内部引用计数进行递减,当其内部引用计数递减至-1
的时候,将完成自身占用资源的释放
上面我们已经探讨了future
和promise
的相关的代码实现,下面我们将讨论一下两者中间进行数据传递的管道的代码实现
关联状态对象的代码实现
clang
版本的关联对象的实现是通过3
层继承来实现的,没层继承都对应了一个功能实现的职责,按照继承树的层级从上至下,这3
层继承分别如下:
__shared_count
:引用计数类,该类用来保存引用计数信息,通过该类内部的引用计数信息来实现自身对象生命周期的管理。用来跟踪链接到自身的promise
和future
对象的数量,当没有任何对象链接自身的时候,进行自身资源的释放__assoc_sub_state
:负责保存管理当前关联状态对象的状态(constructed
/attached
/ready
/deferred
,随后展开讨论),进行线程之间的同步__assoc_state
:负责保存异步操作返回值,并且做最终的封装提供最终的接口给future
和promise
来使用
clang
版本的在很多标准库的实现中都采用了这种通过继承,在每个继承的层级设置不同的职责,最终通过继承树组装起来的方式。而msvc
版本的则更多的采用了组合而非继承的方式。不知道为啥,挺有意思的
__assoc_state
下面,我们通过最下面派生类来进行一步一步的讨论,首先下面看一下__assoc_state
类的数据成员
如上代码所示,该类只有一个在属于当前层级的成员变量,该成员变量位于代码行8
,该成员变量是用来保存异步调用结果的,类型是_Up
该类型是一个类型别名,其别名定义在代码行6
,其是通过返回值类型_Rp
构建了一个对应的内存对齐的数据类型_Up
。这里主要是为了访问效率,内存对齐不在本次讨论范围,这里不展开讨论。理解上可以认为__value_
就相当于是异步操作返回值的类型
下面,我们看一下该继承层级上该类的内部成员函数,首先我们看一下set_value
这个成员方法,代码定义如下
该函数的形参__arg
是用来接受待设置到关联状态对象内部的异步调用结果的值,该函数是一个万能引用,从而根据传入的实参来自动的进行判断传入的实参是以左值引用的方式传入的还是以右值的方式传入的。从而在代码行10
的位置通过std::forward
进行完美转发,从而决定是以移动的方式还是拷贝的方式进行实参的接收
- 代码行
7
:实例化一个局部的unique_lock
类型__lk
临时对象,通过RAII
技术来持有内部的__mut_
互斥锁,当该临时对象离开作用域的时候,即函数结束的时候,会在析构的时候对互斥锁__mut_
进行释放。该__mut_
锁是用来保存内部数据在并发操作的时的多线程并发安全性的。比如异步操作创建线程和异步操作执行线程同时分别通过future
和promise
来从通道中读取和写入值的时候 - 代码行
8
:通过调用成员函数_has_value
来判断当前关联状态对象是否已经被设定值了,即设定了异步调用结果信息,如果已经设定了,此时执行代码行9
,抛出promise already satisfied
异常,用来告诉调用方该promise
早已经被兑现了 - 代码行
10
:通过placement new
操作符,在__value_
上以函数形参__arg
的值,构建异步调用返回值对象。这里之所以采用placement new
是因为用来保存返回值的内存早就已经存在,即内部的__value_
成员变量,这行代码要做的只是在这个内存上进行对象的构建,而不需要通过new
那样先申请内存,然后在申请的内存上进行对象的构建 - 代码行
11
:设置当前关联对象的状态为constructed
,表征当前关联状态对象内部已经构建了异步调用的结果的值;以及设置当前关联对象的状态为ready
,用来表征当前关联状态对象已经处于就绪(ready
)状态,future
对象可以通过get
立刻获取到异步调用的结果信息 - 代码行
12
:调用内部的__cv_
条件变量(conditional variable
),用来通知因为在关联状态对象没有ready
的时候因为调用future
对象的get
方法而阻塞的线程,唤醒这些线程继续获取异步调用的结果信息
下面我们讨论一下上面提到的关联状态对象的内部成员函数move
,为了方便阅读,我们把调用该函数的future
的成员函数重新贴出如下
该move
函数是用来获取关联状态对象内部保存的异步调用的值的,move
的含义是将该值从关联状态对象中移动出来,然后作为get
函数的返回值返回给调用方,完成异步调用结果的传递
- 代码行
5
:声明一个unique_lock
类型的临时对象__lk
来对内部的__mut_
互斥量进行上锁,从而对关联状态对象内部的数据进行并发保护 - 代码行
6
:通过调用__sub_wait
成员函数(该成员函数是从基类继承来的),该成员函数内部对关联状态对象的值的状态进行判断,如果处于就绪状态,即已经可以允许future
来获取值的状态,则会接着向下执行。如果没有处于就绪状态,则会在此处阻塞知道状态就绪,这就是wait
的含义 - 代码行
7~8
:判断关联状态内部是否保存异步调用执行过程中抛出的异常信息,如果有通过代码行8
进行该异常的重新抛出,完成异常从异步调用线程到异步调用创建方线程的传递。此处为异常安全的设计需求,后续展开讨论 - 代码行
9
:以移动的方式返回关联状态对象内部的异步调用结果
该关联对象还有一个函数__on_zero_shared
,该函数是对其基类__shared_count
的同名虚函数的实现(重写)。该函数在内部引用计数递减至-1
的时候,即没有任何promise
或者future
对象链接该关联状态对象的时候,调用该函数,完成对自身资源的释放
代码行5~6
:判断当前关联对象内部是否构建了保存异步调用对象返回值的变量,如果构建了在代码行6
调用其析构函数。注意这里是调用析构函数,并没有释放内存,因为该值是保存在关联状态对象内部成员变量中,会随着状态对象自身的释放而释放
代码行7
:对自身占用的资源进行释放
__assoc_sub_state
按照惯例,我们先看一下该类的数据成员,通过数据成员来看一下该类具体的功能
- 代码行
5
:用来保存异步调用时抛出的异常信息,该异常信息会在异步操作创建方线程调用future::get
方法的时候,重新在该线程抛出,从而完成异常信息在异步调用线程和其创建线程之间传递 - 代码行
6
:互斥量,用来实现关联状态对象内部数据的线程安全,保证并发安全 - 代码行
7
:条件变量,用来实现异步调用线程和异步调用创建方线程之间的同步 - 代码行
8
:用来记录关联状态对象当前的状态,下面我们先展开说一下这个状态
__state_
是用来被记录当前关联状态对象的状态的,其是按照位来存储的,初始状态下__state_
初始值为0
,代表所有状态位都为false
。下面我们来看一下这些状态位,状态位相关定义的代码如下图所示
状态位的每一位的定义是通过枚举进行定义的,当需要设置某些位的时候直接通过或运算和__state_
进行或运算即可
__constructed
:对应bit0
,是用来表示内部已经构建保存了异步调用操作的结果值__future_attached
:对应bit1
,是用来表示当前关联状态对象已经被future
对象所链接(attached
)ready
:用来表示当前关联状态对象处于就绪(ready
)状态,链接该对象的future
对象可以通过调用get
方法来立刻获取到该异步调用的结果相关信息(获取到异步调用的返回值或者异步调用时发生的异常)deferred
:用来表示这是一个推迟执行的“异步调用”,这块后边章节会单独介绍,这种推迟调用,是当前不即可求值也不会在其他线程进行并发求值,而是在使用future
进行get
的时候,在get
的时候,在get
调用的线程进行同步求值
这里可能会好奇,这个__constructed
和ready
这两个状态,可能会有疑问前者状态被置位的时候,说明了该关联状态对象已经被设置了异步调用结果信息,此时不就是就绪状态吗?
其实不是这样的,有的时候设置了异步调用结果之后,并不想使得当前的关联状态对象变成就绪状态,即不想使得此时其他线程通过future::get
方法获取到异步调用结果信息,set_value
方法是设置了值之后就进入了就绪状态,但是下面的这个接口就不是
上面的代码是先设置异步调用结果值到关联状态对象中(对应代码行9
),然后在代码行10
设置关联状态对象中的__constructed
标识位,表征当前已经设置了异步调用结果信息。但是并没有设置ready
状态。代码行11
的作用是设置线程退出的时候钩子,当线程退出的时候再将该状态对象转换为就绪状态
上面set_value_at_exit
函数是为了在异步操作执行的线程退出的时候,在将关联状态对象转变为就绪状态,即异步操作创建方在该异步操作执行线程退出的时候才能获取到该异步调用的结果信息,这里其实是提供了另一种线程同步的方法
下面,我们看一下上面讨论__assoc_state
的时候,讨论接口的时候内部涉及到的该继承层级被使用的函数,首先我们先看一下上面提到的__has_value
成员函数,该函数的源码实现如下图所示
如上图所示,该函数是用来判断当前关联状态对象内部有没有保存异步调用执行结果的信息,注意这里面异步调用结果信息可以是当异步调用成功执行之后得到的返回值,也可以是异步调用没有执行完毕执行过程中抛出的异常。所以该函数的内部实现是通过查看__state_
的__constructed
的状态位或者内部__exception_
是否保存了异常信息
下面,我们在看一下其他的成员函数,首先我们看一下__attach_future
成员函数。在看该成员函数的具体实现,我们先看一下该成员函数的使用,该成员函数是用来将future
链接到该关联状态对象的信息登记到该关联状态对象中的,当future
对象构建的时候,会调用该函数,如下图所示
当future
链接一个关联状态对象的时候,会调用该关联状态对象的成员函数__attach_future
来通知关联状态对象,有一个future
链接他,下面我们看一下该成员函数内部实现,如下图所示
- 代码行
3
:通过实例化一个临时lock_guard
的对象__lk
来进行互斥量上锁,用来保证关联状态对象的并发安全 - 代码行
4~6
:用来判断当前关联状态对象是否被其他future
对象所链接,如果已经有其他future
对象链接,则执行代码行6
抛出异常,代表该关联状态对象早已经被链接 - 代码行
7
:调用__add_shared
成员函数,该成员函数来自于__shared_count
,其作用是递增内部计数。表示当前增加了一个future
链接到了该对象,这个引用计数是为了管理关联状态对象的生命周期 - 代码行
8
:将内部__state_
变量中的记录关联状态对象是否被future
对象所链接的状态位置位
下面,我们看一下在__assoc_state
章节,讨论move
函数时,该函数内部调用到的__sub_wait
的函数,为了方便阅读,我们先将move
的调用首先贴出如下
如上面代码行6
,在进行返回关联状态对象的内部存储的异步调用的结果信息之前,需要先调用__sub_wait
函数,如果当前关联状态对象没有就绪(ready
)的时候,需要在该函数内部阻塞等待,下面我们就看一下该函数的内部实现
- 代码行
4
:判断当前关联状态对象是否处于就绪状态,如果处于就绪状态,函数直接返回。代表外层的future::get
方法可以继续获取内部的值 - 代码行
6
:通过__state_
判断当前关联状态对象是否是推迟求值,如果是执行代码行8~10
,如果不是执行代码行13~14
- 代码行
8~10
:首先清除内部存储的是否是推迟求值的标志位,然后释放用来保护关联状态变量内部数据并发安全的锁,然后在代码行10
执行推迟求值操作(相当于异步操作的函数),使得“异步操作”调用方在调用future::get
方法时在当前线程中执行这个求值操作。这里在求值执行之前之所以要解锁,是因为这里的锁事为了保护关联状态对象内部自身的状态,而不是这个求值调用本身 - 代码行
13~14
:如果不是推迟求值,而是异步调用,此时未处于就绪状态,需要调用条件变量的wait
函数,等待就绪通知
__shared_count
这个对像内部维护了一个引用计数,当关联状态对象被一个promise
和future
对象所链接的时候,会通过调用其内部的__add_shared
对引用计数进行递增1
操作。当某个对象断开链接的时候,会调用其内部的__release_shared
对引用计数进行递减1
操作。当引用计数的递减到-1
的时候,释放关联状态对象。为什么是-1
因为当第一个promise
或者future
对象链接该关联状态对象的时候,引用计数对应的值为0
限于篇幅此处就不展开讲了,要想知道该类的细节可以参考下面文章,shared_point
对象内部就是用该类来管理内部引用计数的
孙梓轩:源码解析:shared_ptr是如何实现共享对象所有权的?8 赞同 · 0 评论文章编辑
future和promise之间的并发安全和线程同步
从上面讨论我们知道,异步操作创建方和异步操作执行方都是是通过其各自的future/promise
对象共同链接的关联状态对象进行同步和信息传递的
通过上面对于关联状态对象的讨论,我们可以看出其API
的内部是通过互斥量对内部状态进行保护,从而实现了线程安全。通过条件变量来实现的线程同步。并且一个关联状态对象在一个时刻只能被一个future
和一个promise
所链接,如果被多个链接则会抛出异常
CPP Reference中有一句话刻意描述了这句话,截图如下所示
上面的最后一段话,中文含义是:注意std::future
所引用的共享状态是不和任何其他异步操作所返回的future
对象所共享的(与之对应的是std::shared_future
)
并且异步操作创建方只能调用一次future::get
来获取异步调用的结果信息,这是因为future::get
函数内部,会断开future
对象与关联状态对象之间的链接,如下代码行5
future对象析构时如果没有调用get接口会阻塞当前进程吗?
从我们前面讨论的future
源码实现中,可以看到,在当前clang
版本(不一定是最新,感兴趣的可以去下载最新的实现看看)的实现中是不阻塞的,为了方便阅读,重新贴出析构函数的源码实现如下
其内部先判断了当前有没有链接关联状态对象,如果链接了调用其__release_shared
成员函数,该成员函数是非阻塞的,其内部仅仅是原子的对引用计数进行递减
但是,你可能从其他的帖子中听过future
的坑,future
如果没有调用get
会阻塞异步操作创建方的线程,在future
被析构的时候。下面我们就到 CPP Reference上看一下C++
标准关于此处的规定,网站截图如下
其中文含义为:
释放任何共享状态(就是我们上面提到的关联状态对象)。这意味着:
- 如果当前对象是最后一个链接到该共享状态的对象,则这个共享对象将会被销毁
- 当前对象放弃其对共享状态的引用(即断开和关联状态对象的链接)
- 此操作不会因为共享状态不处于就绪状态而阻塞,除非当下面所有条件被满足
- 这个共享状态是通过调用
std::async
创建的 - 共享状态目前没有处于就绪状态
- 当前对象是最后一个引用该共享状态对象的对象
实际上,此操作仅仅在任务的启动策略是std::launch::async
才会产生阻塞,因为这是被运行时操作系统所决定的或者是因为是在调用std::async
所指定的
异常安全
promise
和future
进行异步调用结果信息同步的这个机制,从代码上的设计就是异常安全的。这里面的异常安全设计主要在两个方向:
- 首先相关的
API
接口和函数实现需要是异常安全的,即软件内部如果出现异常的时候,函数异常返回的时候不能出现内存泄漏,资源未正确释放等异常情况 - 其次异步调用的时候,如果产生异常可以通过
promise
和future
以及关联状态对象,将异常从异步操作执行线程传递到异步操作创建方所在线程中
下面,我们就逐步来看上面亮点。对于第1
点为了保证在函数异常返回的时候,不能出现资源泄露。通常为了实现这点有一个有效的途径就是在函数内使用RAII
计数来管理资源
RAII
,资源申请即初始化(Resource Acquisition Is Initialization
),通过对象创建(申请)的时候会自动调用构造函数,从而在构造函数中对被管理资源进行占有操作(比如在构造函数中进行互斥量上锁,即锁的占有);对象销毁的时候会自动调用析构函数,从而在析构函数完成对被管理资源的释放(比如在构造函数中进行互斥量解锁,即锁的释放)
下面,我们以future::get
函数为例,来探讨该函数实现对于异常安全的考量,代码截图如下
上面代码行5
,这里就是采用了RAII
的计数,实例化了一个__
对象。将__state_
托管给该unique_ptr
指针,这个unique_ptr
是独占所有权的共享指针,其构造函数没有对资源进行操作,在析构函数对资源进行了释放,通过第2
个模版参数传入自定义资源释放操作,在该函数中会对__state_
进行引用计数递减。即当函数执行出现异常返回的时候,会自动对关联状态对象内部的引用计数进行递减操作
下面,我们看一下这个函数调用链中是否有有抛出异常的代码。上面抛出异常的代码就在代码行8
的move
函数的调用中。下面我们重新贴出该函数的代码实现,如下所示
上面代码行7~8
,当获取异步调用结果信息的时候,发现了目前关联状态对象被设置了异常信息的时候,在代码行8
中重新抛出该异常
下面,我们探讨一下异步操作执行方线程线程传递给异步操作创建方的代码实现。在讨论实现之前,我们来讨论一下,为什么要进行一场信息传递
当一个函数抛出异常的时候,该异常会沿着调用链逐级向上返回,当异常返回到调用链的最高层级这个逐级向上返回的过程中,没有对异常进行捕捉处理,此时会导致程序终止。这里面异常是沿着调用链逐级向上传递的,这也就说明异常只能在产生异常的线程内部进行逐级向上传递(因为函数调用链的各个层级必然属于一个线程)
而这里,构建一个future
和promise
传递异常的通道的原因是由于异步调用是在另外一个线程执行异步操作,但是在另外一个线程进行异步调用结果信息获取,如果异步调用执行过程中出现异常,那么此时这个异常信息也是异步调用执行结果的一种表现形式,那么此时如果不捕获这异常通过构建的通道传递给异步调用结果信息使用线程,这个异常将会在异步调用线程被传递,在异步调用结果使用线程中无法捕捉这一异常信息(因为不会传递到该线程)。那么该线程就无法了解异步调用操作异常返回这一信息
下面,我们看一下实例代码,来看一下当异步调用产生异常的时候,如何进行传递。限于篇幅讨论,这里仅仅贴出代码不展开讨论了,代码和运行结果的截图如下
终端输出如下所示
this is a test exception