我撸了一个函数执行包装器后发现……

什么?函数包装器?那不是std::function么!好吧,今天提的这个“函数执行包装器”不是简单的std::function,但是和std::function也有很大关系,会用到它的能力。

事情是这样的,最近在写一个分布式通讯组件,组件会起后台线程异步接收RPC调用请求,然后在每帧同步地调用所请求的函数。本来后台线程只承担数据接收,每帧同步的时候对接收的数据做反序列化,获取出要调用的函数和函数实参,再调用相应的函数,实现RPC。然而,在实际的一个大系统中,主线程每帧的逻辑任务量往往很大,而后台接收线程相对空闲,于是我打算让后台线程多干点活,在后台线程接收到数据后就做好找函数和反序列化函数参数的工作,然后打包到函数执行包装器中,主线程在每帧同步的时候只把函数执行包装器组同步过来,逐一运行即可,不需要再占用主线程来做反序列化和找函数的工作。

pic_action_executor

要完成上述的任务,我们得需要一个通用的函数执行包装器,通用的意思是能用vector之类的容器存储下来的。单纯的std::function做不到这一点,因为每个函数的参数列表不一致,特例化出来的std::function是不同类型的。除此之外我们还需要把执行函数用到的参数先缓存进我们的包装器里,等到实际运行的时候再使用它们。

通用的函数执行包装器,本来我的想法是再包装一层std::function<void()>,把接收到的数据反序列化后,映射到对应函数,然后把对应函数的执行过程封装到void()的lambda表达式里,对应的反序列化出来的参数,通过移动捕获的方式传递到lambda表达式中,这样这些参数就变成了自由变量,所有权转移到lambda表达式里。

后来想到了一个更简单、逻辑更清晰一些的方法。

首先先做一个ActionExecutor基类,这个基类的Execute函数将承载着在主线程中同步执行函数的任务,主线程不需要关心里面执行的是什么函数,它只需要同步执行函数。

class ActionExecutor {
public:
    virtual void Execute() = 0;
};

Action类是一个模板类,用来保存将要执行的函数和参数,继承Execute并在Execute中实现调用保存下来的函数,传递缓存在类中的参数。

#include <functional>

template <class ...Args>
class Action : public ActionExecutor {
public:
    using ArgsTuple = std::tuple<Args...>;

    template <typename Func>
    Action(Func&& func, ArgsTuple&& args)
        : func(std::forward<Func>(func))
        , args(std::forward<ArgsTuple>(args))
    {
    }

    template <typename Func>
    explicit Action(Func&& func, Args&& ...args)
        : func(std::forward<Func>(func))
        , args(std::forward<Args>(args)...)
    {
    }

    void Execute() override
    {
        Invoke(func, args);
    }

protected:
    template <typename Func>
    inline void Invoke(Func& func, ArgsTuple& argsTuple)
    {
        constexpr auto Size = std::tuple_size<
            typename std::decay<ArgsTuple>::type>::value;
        InvokeImpl(func, argsTuple, std::make_index_sequence<Size>{});
    }

    template <typename Func, std::size_t ...Index>
    inline void InvokeImpl(Func& func, ArgsTuple& argsTuple,
        std::index_sequence<Index...>)
    {
        (void)argsTuple; // Avoid the compilation warning
        // that `the variable has been defined but not used`
        // if the number of parameters of the function is 0.
        func(std::get<Index>(argsTuple)...);
    }

private:
    std::function<void(Args...)> func;
    std::tuple<Args...> args; // ArgsTuple
};

最后,我们做一个帮助函数来帮助我们更好地生成一个Action。

// MakeAction --> ActionExecutor*

template <typename Func, typename ...Args>
ActionExecutor* MakeAction(Func&& func, std::tuple<Args...>&& args)
{
    return new Action<Args...>(
        std::forward<Func>(func),
        std::forward<std::tuple<Args...>>(args));
}

template <typename Func, typename ...Args>
ActionExecutor* MakeAction(Func&& func, Args&& ...args)
{
    return new Action<Args...>(
        std::forward<Func>(func),
        std::forward<Args>(args)...);
}

接下来,就可以愉快地使用函数执行包装器啦(注意实参的所有权问题,一般如果参数是引用,需要std::move转移所有权,否则有可能会出现引用的对象生命周期结束已经被释放的问题,具体使用场景具体分析)。

std::mutex mutex;
std::vector<std::unique_ptr<ActionExecutor>> actions;
...
{
    std::lock_guard<std::mutex> locker(mutex);
    actions.emplace_back(std::make_unique(MakeAction(函数名, 实参)));
}
...
...
{
    std::lock_guard<std::mutex> locker(mutex);
    for (auto& action : actions) {
        action->Execute();
    }
}
...

等等…我究竟干了啥……突然发现我好像实现了一遍std::bind做的事情,std::bind就能完成我们想要的东西呀!

来看看std::bind干了啥,和MakeAction简直雷同(MakeAction和它简直雷同)。

pic_action_executor_1

std::bind后会返回一个_Binder,我们看看_Binder的实现。_Binder中用_Mypair来存储要执行的函数和实参,构造函数中转发到_Mypair中储存起来,我们的Action和它也几乎一致。

pic_action_executor_2

嗐,为啥不用标准库中现成的std::bind呢,改一改(同在Action的实现时所说的一样,我们还是要注意实参的所有权问题):

std::mutex mutex;
std::vector<std::function<void()>> actions;
...
{
    std::lock_guard<std::mutex> locker(mutex);
    actions.emplace_back(std::bind(&函数名, 类实例(如果是类函数), 实参));
}
...
...
{
    std::lock_guard<std::mutex> locker(mutex);
    for (auto& action : actions) {
        action();
    }
}
...

也算是实现了一遍标准库的bind和Binder了,摔桌子 (`□′)╯┴┴


本文为原创内容,遵循CC BY-ND 4.0协议,署名-禁止演绎。
转载请注明出处:https://tis.ac.cn/blog/kongdeyou/action_executor/

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注