写点什么

一个 cpp 协程库的前世今生(十九)event

作者:SkyFire
  • 2022 年 1 月 14 日
  • 本文字数:2167 字

    阅读完需:约 7 分钟

一个cpp协程库的前世今生(十九)event

为了防止大家找不到代码,还是先贴一下项目链接:


GitHub - skyfireitdiy/cocpp at cocpp-0.1.0


在软件开发过程中,经常会出现模块依赖复杂,增加模块间的耦合性的场景。这时,回调函数可以解决一部分问题,但是使用回调函数扩展性比较差,通常一个位置的回调函数只能设置一个,当一个位置已经注册了一个回家函数的时候,想要更改他的行为就会比较麻烦。


本文将展示一个 cocpp 中使用的回调函数技巧,cocpp 中将他命名为 event,但是这个传统意义上的事件又是不一样的,他没有事件循环,但是使用起来是类似的,因此将其称为事件。

接口

event 的接口定义在 include/cocpp/comm/co_event.h:


template <typename... Args>class co_event final : private co_noncopyable{private:    // FIXME: 此处如果使用unordered_map,则会有低概率的崩溃问题,原因未知    std::map<int, std::function<void(Args... args)>> cb_list__;    mutable co_spinlock                              mu_cb_list__ { co_spinlock::lock_type::in_thread };    co_event_handler                                 current_handler__ { 0 };
public: co_event_handler sub(std::function<void(Args... args)> cb); void pub(Args... args) const; void unsub(co_event_handler h);};
复制代码


可以看出这是一个模板类。一共只有三个接口:


  • sub 订阅一个事件,参数为事件处理函数,返回值为订阅的 ID

  • pub 发布一个事件,参数即事件的参数

  • unsub 取消订阅一个事件,参数为从 sub 返回的 ID


接下来再看一下成员变量:


  • cb_list__ 事件处理函数的集合,也就是说同一个事件可以被多个事件处理函数处理(这弥补了普通回调函数扩展性差的缺陷)。

  • mu_cb_list__ 保护处理函数集合的自旋锁。

  • current_handler__ 当前的最大 ID,用于为下一个事件处理函数分配 ID


除此之外,为了方便使用,在 include/cocpp/comm/co_event.h 中还定义了一个宏:


#define RegCoEvent(eventName, ...)       \private:                                 \    co_event<__VA_ARGS__> eventName##__; \                                         \public:                                  \    co_event<__VA_ARGS__>& eventName()   \    {                                    \        return eventName##__;            \    }
复制代码


这个宏的作用是生成一个 event 实例,并添加一个成员函数获取这个实例。

实现

接下来我们看一下实现,因为是模板类,因此它的实现依然在 include/cocpp/comm/co_event.h 中。

sub

sub 的实现如下:


template <typename... Args>co_event_handler co_event<Args...>::sub(std::function<void(Args... args)> cb){    std::lock_guard<co_spinlock> lck(mu_cb_list__);    cb_list__[current_handler__] = cb;    return current_handler__++;}
复制代码


本质上就是在回调函数列表中添加一个项,并返回对应的 ID。

unsub

unsub 对应的实现如下:


template <typename... Args>void co_event<Args...>::unsub(co_event_handler h){    std::lock_guard<co_spinlock> lck(mu_cb_list__);    cb_list__.erase(h);}
复制代码


根据传入的 ID 从回调函数的列表(实际上是一个 map)中,删除对应的回调函数。

pub

pub 的实现其实就是调用回调函数列表中的回调函数:


template <typename... Args>void co_event<Args...>::pub(Args... args) const{    std::lock_guard<co_spinlock> lck(mu_cb_list__);    for (auto& [_, cb] : cb_list__)    {        cb(args...);    }}
复制代码


该函数会遍历回调函数列表,然后依次执行的回调函数。因为我们使用的是 map 来存储回调函数,并且我们的 ID 是从小到大递增的,因此这里的回调函数执行顺序其实就是 sub 的顺序(除非 sub 的数量发生了整数溢出)。

使用

接下来我们看一下 cocpp 中如何使用 event:

注册事件

以下代码片段见 include/cocpp/core/co_ctx.h:


class co_ctx final : private co_noncopyable{    // ...    RegCoEvent(priority_changed, int, int);              // 原优先级    // ...};
复制代码


此处向 ctx 类中注册了 priority_changed 事件,该事件携带两个 int 类型的参数。

注册事件处理

以下代码片段见 source/cocpp/core/co_manager.cpp:


void co_manager::subscribe_ctx_event__(co_ctx* ctx){    ctx->priority_changed().sub([ctx](int old, int new_) {        ctx->env()->handle_priority_changed(old, ctx);    });}
复制代码


这里为 ctx 的 priority_changed 事件注册了处理函数,此处为一个 lambda 表达式。

发布事件

以下代码片段见 source/cocpp/core/co_ctx.cpp:



void co_ctx::set_priority(int priority){ // ... if (old_priority != priority__) { lock.unlock(); // 在priority_changed 事件处理中,可能会修改priority__,所以需要解锁 priority_changed().pub(old_priority, priority__); lock.lock(); }}
复制代码


当优先级被修改时,就会调用上面注册的事件处理函数。


如果我们想监控每一个 ctx 优先级的变化情况,我们可以继续 sub priority_changed 这个事件,这就是扩展性的体现。

总结

回调函数在降低耦合的场景下很有用,但是其自身具有一定的局限性,扩展性不好。通过对它进行封装,我们既可以得到低耦合又可以得到扩展性。

发布于: 刚刚阅读数: 2
用户头像

SkyFire

关注

这个cpper很懒,什么都没留下 2018.10.13 加入

会一点点cpp的苦逼码农

评论

发布
暂无评论
一个cpp协程库的前世今生(十九)event