std::async 与 std::future 源码解析
前言
和之前一样的,我们以 MSVC STL 的实现进行讲解。
std::future,即未来体,是用来管理一个共享状态的类模板,用于等待关联任务的完成并获取其返回值。它自身不包含状态,需要通过如 std::async 之类的函数进行初始化。std::async 函数模板返回一个已经初始化且具有共享状态的 std::future 对象。
因此,所有操作的开始应从 std::async 开始讲述。
需要注意的是,它们的实现彼此之间会共用不少设施,在讲述 std::async 源码的时候,对于 std::future 的内容同样重要。
MSVC STL 很早之前就不支持 C++11 了,它的实现完全基于 C++14,出于某些原因 C++17 的一些库(如
invoke, _v 变量模板)被向后移植到了 C++14 模式,所以即使是 C++11 标准库设施,实现中可能也是使用到了 C++14、17 的东西。注意,不用感到奇怪。
std::async
std::async_EXPORT_STD template <class _Fty, class... _ArgTypes>
_NODISCARD_ASYNC future<_Invoke_result_t<decay_t<_Fty>, decay_t<_ArgTypes>...>> async(
launch _Policy, _Fty&& _Fnarg, _ArgTypes&&... _Args) {
// manages a callable object launched with supplied policy
using _Ret = _Invoke_result_t<decay_t<_Fty>, decay_t<_ArgTypes>...>;
using _Ptype = typename _P_arg_type<_Ret>::type;
_Promise<_Ptype> _Pr(
_Get_associated_state<_Ret>(_Policy, _Fake_no_copy_callable_adapter<_Fty, _ArgTypes...>(
_STD forward<_Fty>(_Fnarg), _STD forward<_ArgTypes>(_Args)...)));
return future<_Ret>(_From_raw_state_tag{}, _Pr._Get_state_for_future());
}
_EXPORT_STD template <class _Fty, class... _ArgTypes>
_NODISCARD_ASYNC future<_Invoke_result_t<decay_t<_Fty>, decay_t<_ArgTypes>...>> async(
_Fty&& _Fnarg, _ArgTypes&&... _Args) {
// manages a callable object launched with default policy
return _STD async(launch::async | launch::deferred, _STD forward<_Fty>(_Fnarg), _STD forward<_ArgTypes>(_Args)...);
}这段代码最直观的信息是,函数模板 std::async 有两个重载,其中第二个重载只是给了一个执行策略并将参数全部转发,调用第一个重载。也就是不指明执行策略的时候就会匹配到第二个重载版本。因此我们也只需要关注第二个版本了。
模板参数和函数体外部信息:
_EXPOPT_STD是一个宏,当_BUILD_STD_MODULE宏定义且启用了 C++20 时,会被定义为export,以便导出模块;否则它为空。_Fty表示可调用对象的类型。_ArgTypes是一个类型形参包,表示调用该可调用对象所需的参数类型。_NODISCARD_ASYNC是一个宏,表示属性[[nodiscard]],用于标记此函数的返回值不应被忽略。
函数返回类型:
虽然看起来复杂,但实际上是通过
_Invoke_result_t获取可调用对象的返回类型。与标准库中的std::invoke_result_t基本相同。可以举一个使用
std::invoke_result_t的例子:值得注意的是,所有类型在传递前都进行了
decay处理,也就是说不存在引用类型,是默认按值传递与std::thread的行为一致。函数形参:
launch _Policy: 表示任务的执行策略,可以是launch::async(表示异步执行)或launch::deferred(表示延迟执行),或者两者的组合。_Fty&& _Fnarg: 可调用对象,通过完美转发机制将其转发给实际的异步任务。_ArgTypes&&... _Args: 调用该可调用对象时所需的参数,同样通过完美转发机制进行转发。using _Ret = _Invoke_result_t<decay_t<_Fty>, decay_t<_ArgTypes>...>;using _Ptype = typename _P_arg_type<_Ret>::type;定义
_Ret类型别名,它是使用_ArgTypes类型参数调用_Fty类型的可调用对象后得到的结果类型。也就是我们传入的可调用对象的返回类型;同样使用了_Invoke_result_t(等价于std::invoke_result_t) 与decay_t。其实
_Ptype的定义确实在大多数情况下和_Ret是相同的,类模板 _P_arg_type 只是为了处理引用类型以及 void 的情况,参见_P_arg_type的实现:_Ptype:处理异步任务返回值的方式类型,它在语义上强调了异步任务返回值的处理方式,具有不同的实现逻辑和使用场景。在当前我们难以直接展示它的作用,不过可以推测,这个“P” 表示的是后文将使用的_Promise类模板。也就是说,定义_Ptype是为了配合_Promise的使用。我们将会在后文详细探讨_Promise类型的内部实现,并进一步解释_Ptype的具体作用。
_Promise<_Ptype> _Pr_Promise类型本身不重要,很简单,关键还在于其存储的数据成员。_Promise类模板是对_State_manager类模板的包装,并增加了一个表示状态的成员_Future_retrieved。状态成员用于跟踪
_Promise是否已经调用过_Get_state_for_future()成员函数;它默认为false,在第一次调用_Get_state_for_future()成员函数时被置为true,如果二次调用,就会抛出future_errc::future_already_retrieved异常。这类似于
std::promise调用get_future()成员函数。测试。_Promise的构造函数接受的却不是_State_manager类型的对象,而是_Associated_state类型的指针,用来初始化数据成员_State。这是因为实际上
_State_manager类型的实现就是保有了Associated_state指针,以及一个状态成员:也可以简单理解
_State_manager又是对Associated_state的包装,其中的大部分接口实际上是调用_Assoc_state的成员函数,如:一切的重点,最终在
Associated_state上。
然而它也是最为复杂的,我们在讲
std::thread-构造源码解析 中提到过一句话:了解一个庞大的类,最简单的方式就是先看它的数据成员有什么。
这是
Associated_state的数据成员,其中有许多的bool类型的状态成员,同时最为明显重要的三个设施是:异常指针、互斥量、条件变量。根据这些数据成员我们就能很轻松的猜测出
Associated_state模板类的作用和工作方式。异常指针:用于存储异步任务中可能发生的异常,以便在调用
future::get时能够重新抛出异常。互斥量和条件变量:用于在异步任务和等待任务之间进行同步。当异步任务完成时,条件变量会通知等待的任务。
_Associated_state模板类负责管理异步任务的状态,包括结果的存储、异常的处理以及任务完成的通知。它是实现std::future和std::promise的核心组件之一,通过_State_manager和_Promise类模板对其进行封装和管理,提供更高级别的接口和功能。上图是
_Promise、_State_manager、_Associated_state之间的包含关系示意图,理解这个关系对我们后面非常重要。到此就可以了,我们不需要在此处就详细介绍这三个类,但是你需要大概的看一下,这非常重要。
初始化
_Promie对象:_Get_associated_state<_Ret>(_Policy, _Fake_no_copy_callable_adapter<_Fty, _ArgTypes...>(_STD forward<_Fty>(_Fnarg), _STD forward<_ArgTypes>(_Args)...))很明显,这是一个函数调用,将我们
std::async的参数全部转发给它,它是重要而直观的。_Get_associated_state函数根据启动模式(launch)来决定创建的异步任务状态对象类型:_Get_associated_state函数返回一个_Associated_state指针,该指针指向一个新的_Deferred_async_state或_Task_async_state对象。这两个类分别对应于异步任务的两种不同执行策略:延迟执行和异步执行。其实就是父类指针指向了子类对象,注意
_Associated_state是有虚函数的,子类进行覆盖,这很重要。比如在后续聊std::future的get()成员函数的时候就会讲到这段代码也很好的说明在 MSVC STL 中,
launch::async | launch::deferred和launch::async的行为是相同的,即都是异步执行。_Task_async_state与_Deferred_async_state类型_Task_async_state与_Deferred_async_state都继承自_Packaged_state,用于异步执行任务。它们的构造函数都接受一个函数对象,并将其转发给基类_Packaged_state的构造函数。_Packaged_state类型只有一个数据成员std::function类型的对象_Fn,它用来存储需要执行的异步任务,而它又继承自_Associated_state。我们直接先看
_Task_async_state与_Deferred_async_state类型的构造函数实现即可:_Task_async_state它的数据成员:_Task_async_state的实现使用到了微软自己实现的 并行模式库(PPL),简而言之launch::async策略并不是单纯的创建线程让任务执行,而是使用了微软的::Concurrency::create_task,它从线程池中获取线程并执行任务返回包装对象。this->_Call_immediate();是调用_Task_async_state的父类_Packaged_state的成员函数_Call_immediate。_Packaged_state有三个偏特化,_Call_immediate自然也拥有三个不同版本,用来应对我们传入的函数对象返回类型的三种情况:返回 void 类型
_Packaged_state<void(_ArgTypes...)>
说白了,无非是把返回引用类型的可调用对象返回的引用获取地址传递给
_Set_value,把返回 void 类型的可调用对象传递一个 1 表示正确执行的状态给_Set_value。_Call_immediate则又调用了父类_Associated_state的成员函数(_Set_value、_set_exception),传递的可调用对象执行结果,以及可能的异常,将结果或异常存储在_Associated_state中。_Deferred_async_state并不会在线程中执行任务,但它同样调用_Call_immediate函数执行保有的函数对象,它有一个_Run_deferred_function函数:然后也就和上面说的没什么区别了 。
返回
std::futurereturn future<_Ret>(_From_raw_state_tag{}, _Pr._Get_state_for_future());它选择到了
std::future的构造函数是:_From_raw_state_tag是一个空类,并没有什么特殊作用,只是为了区分重载。_Get_state_for_future代码如下:检查状态,修改状态,返回底层
_State,完成转移状态。总而言之这行代码通过调用
std::future的特定构造函数,将_Promise对象中的_State_manager状态转移到std::future对象中,从而创建并返回一个std::future对象。这使得std::future可以访问并管理异步任务的状态,包括获取任务的结果或异常,并等待任务的完成。
std::future
std::future先前的 std::async 的内容非常之多,希望各位开发者不要搞晕了,其实重中之重主要是那几个类,关系图如下:
_Promise、_State_manager、_Associated_state之间的包含关系示意图。
_Asscociated_state、_Packaged_state、_Task_async_state、_Deferred_async_state继承关系示意图。
这其中的 _Associated_state、_State_manager 类型是我们的核心,它在后续 std::future 乃至其它并发设施都有众多使用。
介绍 std::future 的源码我认为无需过多篇幅或者示例,引入过多的源码实现等等从头讲解,只会让各位开发者感觉复杂难。
我们直接从它的最重要、常见的 get()、wait() 成员函数开始即可。
我们先前已经详细介绍过了 std::async 返回 std::future 的步骤。以上这段代码,唯一的问题是:future.get() 做了什么?
std::future其实还有两种特化,不过整体大差不差。也就是对返回类型为引用和 void 的情况了。其实先前已经聊过很多次了,无非就是内部的返回引用实际按指针操作,返回 void,那么也得给个 1。参见前面的
_Call_immediate实现。
可以看到 std::future 整体代码实现很少,很简单,那是因为其实现细节都在其父类 _State_manager 。然而 _State_manager 又保有一个 _Associated_state<_Ty>* 类型的成员。而 _Associated_state 又是一切的核心,之前已经详细描述过了。
阅读 std::future 的源码你可能注意到了一个问题:*没有 wait()成员函数?
它的定义来自于父类 _State_manager :
然而这还不够,实际上还需要调用了 _Associated_state 的 wait() 成员函数:
先使用锁进行保护,然后调用函数,再循环等待任务执行完毕。_Maybe_run_deferred_function:
_Run_deferred_function 相信你不会陌生,在讲述 std::async 源码中其实已经提到了,就是解锁然后调用 _Call_immediate 罢了。
_Call_immediate就是执行我们实际传入的函数对象,先前已经提过。
在 _Wait 函数中调用 _Maybe_run_deferred_function 是为了确保延迟执行(launch::deferred)的任务能够在等待前被启动并执行完毕。这样,在调用 wait 时可以正确地等待任务完成。
至于下面的循环等待部分:
这段代码使用了条件变量、互斥量、以及一个状态对象,主要目的有两个:
避免虚假唤醒:
条件变量的
wait函数在被唤醒后,会重新检查条件(即_Ready是否为true),确保只有在条件满足时才会继续执行。这防止了由于虚假唤醒导致的错误行为。
等待
launch::async的任务在其它线程执行完毕:对于
launch::async模式的任务,这段代码确保当前线程会等待任务在另一个线程中执行完毕,并接收到任务完成的信号。只有当任务完成并设置_Ready为true后,条件变量才会被通知,从而结束等待。
这样,当调用 wait 函数时,可以保证无论任务是 launch::deferred 还是 launch::async 模式,当前线程都会正确地等待任务的完成信号,然后继续执行。
wait() 介绍完了,那么接下来就是 get() :
在第四章的 “future 的状态变化”一节中我们也详细聊过 get() 成员函数。由于 future 本身有三个特化,get() 成员函数自然那也有三个版本,不过总体并无多大区别。
它们都是将当前对象(*this)的共享状态转移给了这个局部对象 _Local,然后再去调用父类_State_manager 的成员函数 _Get_value() 获取值并返回。而局部对象 _Local 在函数结束时析构。这意味着当前对象(*this)失去共享状态,并且状态被完全销毁。
_Get_value() :
先进行一下状态判断,如果拥有共享状态则继续,调用 _Assoc_state 的成员函数 _Get_value ,传递 _Get_only_once 参数,其实就是代表这个成员函数只能调用一次,次参数是里面进行状态判断的而已。
_Assoc_state 的类型是 _Associated_state<_Ty>* ,是一个指针类型,它实际会指向自己的子类对象,我们在讲 std::async 源码的时候提到了,它必然指向 _Deferred_async_state 或者 _Task_async_state。
_Assoc_state->_Get_value 这其实是个多态调用,父类有这个虚函数:
但是子类 _Task_async_state 进行了重写,以 launch::async 策略创建的 future,那么实际会调用 _Task_async_state::_Get_value :
_Deferred_async_state则没有进行重写,就是直接调用父类虚函数。
_Task 就是 ::Concurrency::task<void> _Task;,调用 wait() 成员函数确保任务执行完毕。
_Mybase::_Get_value(_Get_only_once) 其实又是回去调用父类的虚函数了。
_Get_value方法详细解释
状态检查:
如果
_Get_only_once为真并且结果已被检索过,则抛出future_already_retrieved异常。
异常处理:
如果存在存储的异常,重新抛出该异常。
标记结果已被检索:
将
_Retrieved设置为true。
执行延迟函数:
调用
_Maybe_run_deferred_function来运行可能的延迟任务。这个函数很简单,就是单纯的执行延时任务而已,在讲述wait成员函数的时候已经讲完了。
等待结果就绪:
如果结果尚未准备好,等待条件变量通知结果已就绪。(这里和
std::async和std::future的组合无关,因为如果是launch::async模式创建的任务,重写的_Get_value是先调用了_Task.wait();确保异步任务执行完毕,此处根本无需等待它)
再次检查异常:
再次检查是否有存储的异常,并重新抛出它。
返回结果:
如果
_Ty是默认可构造的,返回结果_Result。否则,返回
_Result._Held_value。
_Result 是通过执行 _Call_immediate 函数,然后 _Call_immediate 再执行 _Set_value ,_Set_value 再执行 _Emplace_result,_Emplace_result 再执行 _Emplace_result 获取到我们执行任务的值的。以 Ty 的偏特化为例:
总结
好了,到此也就可以了。
你不会期待我们将每一个成员函数都分析一遍吧?首先是没有必要,其次是篇幅限制。
std::future 的继承关系让人感到头疼,但是如果耐心的看了一遍,全部搞明白了继承关系, std::async 如何创建的 std::future 也就没有问题了。
其实各位不用着急完全理解,可以慢慢看,至少有许多的显著的信息,比如:
sttd::future的很多部分,如get()成员函数实现中,实际使用了虚函数。std::async创建std::future对象中,内部其实也有父类指针指向子类对象,以及多态调用。std::async的非延迟执行策略,使用到了自家的 PPL 库。微软的
std::async策略实现并不符合标准,不区分launch::async | launch::deferred和launch::async。std::future内部使用到了互斥量、条件变量、异常指针等设施。
Last updated