谈C++的future

本篇文章带领大家看下C++中的future相关细则,本文会讨论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的实现,我们前边讲到了future和promise,future和packaged_task,future和async这些都是成对存在的,需要生产者(promise, packaged_task,async)设定一个值并可以通知到future,然后future就能获取到值了,自然而然我们想到可能future和promise等是不是共同拥有一个对象呢,这类就是state,promise等和future使用shared_ptr来持有一个state的对象,一旦promise设定了值,也就是会设定state对象的值,future也就能获取到,如图所示:

谈C++的future_第1张图片

然后我们选择一些关键的代码来详细讲解下(以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主要的缺点是缺少可组合性:
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++技术点。

ref

  • https://zhuanlan.zhihu.com/p/395250667
  • https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3721.pdf
  • https://bartoszmilewski.com/2009/03/03/broken-promises-c0x-futures
  • https://github.com/facebookexperimental/libunifex
  • https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1445r0.pdf

你可能感兴趣的:(C++,c++,future,executor,promise,async)