本篇文章带领大家看下C++中的future相关细则,本文会讨论future的使用实现已经探讨下他的缺点,然后会展望下如何对future进行优化等。
C++11开始提出了future的概念,充当多线程通信中的接收者的角色。需要和其他的概念搭配使用,先来看和promise搭配使用:
#include
#include
#include
#include
void testPromise() {
std::promise p;
std::future fut = p.get_future();
std::thread t([&p](){
std::this_thread::sleep_for(std::chrono::seconds(2));
p.set_value(20);
});
int x = fut.get();
std::cout << "value: " << x << '\n';
t.join();
}
int main() {
testPromise();
return 0;
}
我们简单看下代码,声明promise,通过promise获得相关联的future,然后这里把promise传递给了另外一个线程,为的是当线程接收时可以通知到future。通知的方式使用set_value函数。另外一个线程future调用get函数去获取promise通知给它的值。其中future调用get会阻塞所在的线程,知道有数据传递过来。
可以看到future/promise相当于多线程中一对生产者和消费者的概念。不过需要启动我们自己启动线程并将promise对象传递到线程里。
所以C++也提出了packaged_task来做进一步的优化:
void testPackagedTask() {
std::packaged_task pTask([]()-> int{
std::this_thread::sleep_for(std::chrono::seconds(2));
return 23;
});
std::future fut = pTask.get_future();
std::thread t(std::move(pTask));
int x = fut.get();
std::cout << "value: " << x << '\n';
t.join();
}
可以看到packaged_task比promise更加简便了一些,不需要我们自己创建promise,只需要创建一个函数被packaged_task包裹,然后开启线程去执行,当这个函数返回,future的get会获取到相应的结果。
不过这里还是需要我们去开启线程去执行任务,所以有std::async提供使用:
void testAsync() {
std::future fut = std::async([]() ->int {
std::this_thread::sleep_for(std::chrono::seconds(2));
return 24;
});
int x = fut.get();
std::cout << "value: " << x << '\n';
}
可以看到async同样时返回一个future用来获取数据,不过代码相当的简便了,不用去构造promise,也不用开启线程,get时会阻塞主线程到子线程结束然后获取到数据。
然后我们来看下future的实现,我们前边讲到了future和promise,future和packaged_task,future和async这些都是成对存在的,需要生产者(promise, packaged_task,async)设定一个值并可以通知到future,然后future就能获取到值了,自然而然我们想到可能future和promise等是不是共同拥有一个对象呢,这类就是state,promise等和future使用shared_ptr来持有一个state的对象,一旦promise设定了值,也就是会设定state对象的值,future也就能获取到,如图所示:
然后我们选择一些关键的代码来详细讲解下(以gcc实现代码为准):
template
class future : public __basic_future<_Res>
{
friend class promise<_Res>;
template friend class packaged_task;
template
friend future<__async_result_of<_Fn, _Args...>>
async(launch, _Fn&&, _Args&&...);
typedef __basic_future<_Res> _Base_type;
typedef typename _Base_type::__state_type __state_type;
explicit
future(const __state_type& __state) : _Base_type(__state) { }
public:
// ...
/// Retrieving the value
_Res get() {
return std::move(this->_M_get_result()._M_value());
}
};
这么一看代码很简单,future继承了__basic_future,关于这个__basic_future我们后面再来看。然后就是声明promise,packaged_task,async时future的友元,那就是这三个可以访问future的私有函数。一个构造函数的参数的类型是__state_type,也就是我们前边说的state。提供了get函数,调用自己的_M_get_result函数,也是在__basic_future实现的,我们去__basic_future看下这个函数和具体的__state_type类型。
template
class __basic_future : public __future_base
{
protected:
typedef shared_ptr<_State_base> __state_type;
typedef __future_base::_Result<_Res>& __result_type;
private:
__state_type _M_state;
public:
// ...
void wait() const {
_M_state->wait();
}
protected:
/// Wait for the state to be ready and rethrow any stored exception
__result_type _M_get_result() const {
_Result_base& __res = _M_state->wait();
if (!(__res._M_error == 0))
rethrow_exception(__res._M_error);
return static_cast<__result_type>(__res);
}
// ...
};
这里就知道了__state_type是shared_ptr<_State_base>类型,看起来确实是用shared_ptr封装了_State_base类型,我们看到有个base字样,猜测是future使用state类是父类,而promise,packaged_task,async使用的state是子类,这个猜测我们到后边来验证。_M_get_result或者wait都是调用了state的wait()函数,据此获得了结果,也就是说调用state的wait函数会一直阻塞到结果产生,并返回给用户。
因为state中会有一些生产者和消费者都有的一些逻辑,我们后边看state的源码,我们先来看下promise的代码:
template
class promise
{
typedef __future_base::_State_base _State;
typedef __future_base::_Result<_Res> _Res_type;
typedef __future_base::_Ptr<_Res_type> _Ptr_type;
template
friend class _State::_Setter;
friend _State;
shared_ptr<_State> _M_future;
_Ptr_type _M_storage;
public:
promise():
_M_future(std::make_shared<_State>()),
_M_storage(new _Res_type()) {}
// Retrieving the result
future<_Res> get_future()
{ return future<_Res>(_M_future); }
// Setting the result
void set_value(const _Res& __r) {
_M_future->_M_set_result(_State::__setter(this, __r));
}
void
set_exception(exception_ptr __p) {
_M_future->_M_set_result(_State::__setter(__p, this));
}
};
我这里也只是列出来了一些帮助我们理解的关键代码,哈~ 可以看到promise使用state也是_State_base,看来针对promise我们猜测是错了。这里也有一个比较有意思的是,promise把state居然命名成future,大家看的时候不要懵了。get_future函数这里是通过state构造了一个future返回。_M_storage就是存放结果的字段。然后就是promise的set_value和set_exception函数了,我们这里只看set_value,也还是调用了state里的_M_set_result函数。不过也不是简单的讲值设定进去,而是构造__setter这个对象然后再设定。另外我们到这里发现_M_storage还未使用,答案就是在这个__setter里。
然后我们就来看state这个类做了什么:
class _State_baseV2
{
typedef _Ptr<_Result_base> _Ptr_type;
enum _Status : unsigned {
__not_ready,
__ready
};
_Ptr_type _M_result;
__atomic_futex_unsigned<> _M_status;
public:
_State_baseV2() noexcept : _M_result(), _M_status(_Status::__not_ready)
{ }
_Result_base& wait() {
_M_complete_async();
_M_status._M_load_when_equal(_Status::__ready, memory_order_acquire);
return *_M_result;
}
void _M_set_result(function<_Ptr_type()> __res, bool __ignore_failure = false) {
bool __did_set = false;
call_once(_M_once, &_State_baseV2::_M_do_set, this,
std::__addressof(__res), std::__addressof(__did_set));
if (__did_set)
_M_status._M_store_notify_all(_Status::__ready,
memory_order_release);
}
// set lvalues
template
struct _Setter<_Res, _Arg&> {
// Used by std::promise to copy construct the result.
typename promise<_Res>::_Ptr_type operator()() const {
_M_promise->_M_storage->_M_set(*_M_arg);
return std::move(_M_promise->_M_storage);
}
promise<_Res>* _M_promise;
_Arg* _M_arg;
};
template
static _Setter<_Res, _Arg&&> __setter(promise<_Res>* __prom, _Arg&& __arg) {
return _Setter<_Res, _Arg&&>{ __prom, std::__addressof(__arg) };
}
private:
void _M_do_set(function<_Ptr_type()>* __f, bool* __did_set)
{
_Ptr_type __res = (*__f)();
*__did_set = true;
_M_result.swap(__res); // nothrow
}
virtual void _M_complete_async() { }
};
state中有两个成员变量,result和status,result比较好理解的,就是结果的存放对象,status的类是__atomic_futex_unsigned,这个是gcc对linux操作系统futex进行的一层封装,可以理解成是一个原子的变量,它的值是not_ready或者ready。初始化是not_ready,等到被promise等设定后就会变成ready。除此之外,__atomic_futex_unsigned对象还有着条件变量的功能,也就是说可以使得线程阻塞直到满足条件被唤醒,实现的原理都是futex这个linux的概念实现。和我们平常条件变量及unique_lock组合性能更好一些,不过这里只能支持unsigned这样的变量。
记下来看wait函数,先调用_M_complete_async这个函数,这个在state_base的父类中是个空函数。然后就调用status的_M_load_when_equal函数,表示只有等到他的值等于ready时才返回,不然一直阻塞在这里。那我们这里就知道future调用wait相当于需要等status等于ready才返回。
最后看下_M_set_result函数,这个函数接收的参数是一个函数对象,这个函数对象的返回值正好是最终的结果。然后就是调用_M_do_set函数,完成后使用status的notify函数来通知另外阻塞的一端。_M_do_set这个函数其实也没有做什么,就是调用一下返回值是最终结果的函数对象,并将结果赋予state的成员变量_M_result,这样future调用get就直接返回这个_M_result值即可。那么关键点还是在这个函数对象这里,我们再结合之前promise的代码看下这个函数对象如何构造,promise调用set_value中,通过_State::__setter函数构造一个_Setter对象出来,并且将promise对象和最终结果指针传递给_Setter对象,_Setter类对象是一个仿函数(重载括号操作符),那也就是说最终传递给_M_set_result的函数对象就是这个_Setter仿函数,当它被调用时才将最终的返回值设定到promise的_M_storage中,也就是存放最终结果的对象中。
那这样下来就做到了返回值设定到promise然后move给state,最终返回给future。
我理解promise的结果的设定要到state做而不是在promise中设定好了再去到state中通知的原因可能是两个,第一个是受限于future只能wait一次,也就是promise只能设定一次值,所以需要在state中统一处理这个call_once的操作,第二个是在构造setter的时候回去检查promise的state是否存活,如果不存活就抛出异常,省去了一次最终结果的拷贝。
这里我们看完了future,promise,state的实现,接下来就简单看下async的实现吧:
template
_GLIBCXX_NODISCARD future<__async_result_of<_Fn, _Args...>>
async(launch __policy, _Fn&& __fn, _Args&&... __args)
{
std::shared_ptr<__future_base::_State_base> __state;
if ((__policy & launch::async) == launch::async)
{
__state = __future_base::_S_make_async_state(
std::thread::__make_invoker(std::forward<_Fn>(__fn),
std::forward<_Args>(__args)...)
);
}
return future<__async_result_of<_Fn, _Args...>>(__state);
}
我们这里只看async模式下的正常情况,可以看到async的state是通过_S_make_async_state构造的,参数传递了一个将函数和参数封装成一个可以被调用的invoker。然后使用这个state再构造出来future返回。那我们看下_S_make_async_state这个函数实现:
template
inline std::shared_ptr<__future_base::_State_base>
__future_base::_S_make_async_state(_BoundFn&& __fn)
{
typedef typename remove_reference<_BoundFn>::type __fn_type;
typedef _Async_state_impl<__fn_type> __state_type;
return std::make_shared<__state_type>(std::move(__fn));
}
这里其实也没有什么实际的内容,就是构造了一个_Async_state_impl的state对象,构造函数将传经来的可以被调用invoker作为参数。继续跟踪看下_Async_state_impl这个类:
template
class __future_base::_Async_state_impl final
: public __future_base::_Async_state_commonV2
{
public:
explicit _Async_state_impl(_BoundFn&& __fn)
: _M_result(new _Result<_Res>()), _M_fn(std::move(__fn))
{
_M_thread = std::thread{ [this] {
_M_set_result(_S_task_setter(_M_result, _M_fn));
}};
}
private:
typedef __future_base::_Ptr<_Result<_Res>> _Ptr_type;
_Ptr_type _M_result;
_BoundFn _M_fn;
};
我们会发现_Async_state_impl继承了_Async_state_commonV2,_Async_state_commonV2然后就会继承state_base。_Async_state_impl的父类_Async_state_commonV2中比较关键的实现是:
class __future_base::_Async_state_commonV2
: public __future_base::_State_base
{
protected:
~_Async_state_commonV2() = default;
virtual void _M_complete_async() { _M_join(); }
void _M_join() { std::call_once(_M_once, &thread::join, &_M_thread); }
thread _M_thread;
once_flag _M_once;
};
提供给子类一个线程对象,构造_Async_state_impl时会启动一个线程执行最开始传进来的函数,并将结果调用state的_M_set_result函数进行设定。还记得我们_M_complete_async函数吗,在future调用wait时,首先会调用_M_complete_async这个函数,在这里可以体现出来,async时会等到开启的线程结束才会从state中获取值,避免线程无人回收出现奔溃等现象。
关于future的相关实现到这里了,大家明白最简单的原理就好:future和其他的生产者共用一个state对象,future通过futex机制的原子变量等待生产者设定值,生产者在完成后调用state的set_result函数进行值的设定并通知原子变量。(gcc的实现)
针对于future主要的缺点是缺少可组合性:
future像是一个构建任务DAG的雏形,不能表达任务的前置与后置的依赖关系。只能阻塞在get后然后才能进行下一步的工作。
举个例子,如果多个线程使用promise返回多个future,需要future返回后进行接下来的步骤,但是不知道哪个future先完成,这样就需要按顺序等待,这时可能等待的future恰好不是最快到来的那个,这时候就会浪费一些宝贵的时间。
1.基于一些可组合性的缺点,一些c++的提案也尝试解决这类的问题:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3721.pdf
提出了then,is_ready,when_any / when_all等函数,then就是本次future执行完接下来的事情,is_ready可以判断future的结果是否到来,when_any可以设定等多个future中任意一个到来结束阻塞。when_all就是等待所有的future结果到来才结束阻塞。
其中最关键的莫过于then:
#include
using namespace std;
int main() {
future f1 = async([]() { return 123; });
future f2 = f1.then([](future f) {
return f.get().to_string(); // here .get() won’t block
});
}
可以看到f1通过async获取到,设定f1的then函数传入lambda,那等到async中的函数体执行完后就会去执行then里边的内容。
有了这些是不是可以完整的进行构建任务并且提升了方便性和性能。
这个本计划要放在
原因是这其中设计无疑是由缺陷的,以上async的实现是积极提交的,积极提交本身没有问题,但是会导致在调用then设定函数时需要加锁,也还得判断上一次的结果是否已经获取到;还有一个问题是目前future的这种设计的state是类型擦除的,包括获取中间的结果,then传入的函数对象等都需要类型擦除的手段进行保存。这在c++这种强大的编译期能力被认为设计是不合理的。
2.进一步c++委员会提出了Executor,Executor可以解决掉以上的两个问题,executor会保存中间(then函数体)每一次的类型。同样Executor创建任务是惰性提交的,自然也就没有了加锁等系列的问题。除此之外,executor还会封装异构编程,以及和coroutine相结合,可惜的是目前还未加入到C++标准(23),类似的实现有libunifex,可以供参考使用。
本文到这里结束,着重讲解了future与promise/packaged_task/async的使用和实现。可以知道future和生产者共享state对象,使用shared_ptr封装。接着讨论了下future使用过程的缺点,以及展望下对future的优化,最后也引出executor这个比较关键的C++技术点。