乐趣区

关于c++:多线程使用libevent那些花里胡哨的技巧

原文

前言

libevent 封装了底层多路复用接口,让咱们能够更不便地跨平台应用异步网络 IO。同时,libevent 还实现了定时工作,应用它咱们就不必本人实现一遍了,还是比拟不便。

libevent 官网提供了 libevent 的教程、libevent 的例子 以及 libevent 的接口文档,写得 相当好。我在浏览过一遍之后,开始尝试应用它实现一个负责与物联网设施通信的接入程序,也就是一般的 TCP/UDP 服务端,承当接管连贯申请、接收数据、下发数据、验证身份、转发 设施申请、治理连贯超时、以及实现一些简略的接口,当然还有其它懒得说的性能。这个程 序跟 nginx 是很像的,之前我间接用 epoll 实现过很多个相似的程序,最近看到 libevent 开发的程序很多,于是开始尝试应用它。

而后遇到了问题。我尝试创立多个 IO 线程,然而事件并不是我设想的那样,event_del() 阻塞了。libevent 的事件操作只能在 dispatch 循环同一个线程中执行,也就是循环退 出后,或者在回调函数中。

一番调试后有了这个 echo-server 例子,以及这篇阐明,记录我调试的过程。

libevent 入门

libevent 有两个概念,event_base 和 event。

event 是事件,能够设定触发条件(可读 / 可写 / 超时 / 信号),以及条件登程后须要 执行的函数。event 相当于 epoll 中的 epoll_event。

事件循环在 event_base 上执行,event_base 里记录了所有事件的触发条件,循环中查看 条件,如果条件满足,则调用 event 中指定的函数。event_base 相当于 epoll 中 epoll_create() 创立的构造。

例如,要读取一个文件描述符 fd,能够创立一个读事件,在事件的回调函数中读 fd:

// 事件的回调函数
void event_cb(evutil_socket_t fd, short what, void* arg) {
  // 在这里读 fd
  // ...
}

void* arg = NULL;
// 创立 event_base
struct event_base* ev_base = event_base_new();
// 创立 event, 能够从一个 event_base 中创立多个 event
struct event* ev = event_new(ev_base, fd, EV_READ, event_cb, arg);

// 把事件注册到循环中
event_add(ev, NULL);

// 开始事件循环,如果事件的条件满足,则调用事件的回调函数
event_base_dispatch(ev_base);

设计构造

有两种形式构造能达到我的目标,一种是一个线程监听事件,线程池处理事件;第二种是多 个线程监听事件,事件登程后间接在本线程中执行。我应用的是第二种。

单事件循环,多解决线程

event_base 操作只能与事件循环在同一个线程中,为了在多线程中都能够进行事件处理,第一个想法是在事件线程中创立 event_base 循环,回调函数中将事件的解决交给线程池。事件循环中存在一个超时事件 A,这个超时事件的回调函数专门负责执行线程池发过来的操作 事件的代码。如果在线程池中还须要操作事件,则将操作事件的代码发给事件线程,并将超 时事件激活以执行这些代码。

我在应用 epoll 时常常应用这种形式,一个线程监听事件,事件触发后再交由线程池解决,如果要增加或操作事件,则把操作函数发到队列中,由监听事件的线程执行。这种形式须要 在多个线程中交换,会造成一些性能损失,但在我理论的我的项目中,这些性能损失跟业务耗费 的性能比起来微不足道。但这次开发的程序我还是用了另一种模式。

多事件循环

创立多个事件线程,每个线程创立一个 event_base 事件循环,事件触发时间接在事件线 程中执行。

理论的我的项目中,事件触发后,不只进行 IO 操作,还有很多阻塞的工作须要解决,最常见的是 申请数据库。数据库操作也能够写成异步 IO 放在事件循环中,但为了不便,我都把数据库操 作放到线程池中运行,运行完结后将事件操作放到队列,向上一个构造一样,由一个超时任 务来处理事件操作。

代码阐明

代码都上传了,去除了业务相干的逻辑,只实现了 echo-server 性能。

buffer

buffer.cc 和 buffer.h 实现了可变长度的读写缓冲区。libevent 有 evbuffer 构造,本人实现 buffer 是因为业务的程序中不止用了 libevent,还有很多遗留的 IO 代码,为 了在 IO 当前对立业务接口,还是沿用本人实现的 buffer。libevent 的实现是比我的实 现更不便高效的,比我不知高到哪里去,今后我会思考应用它。

buffer 实质上是个缓存队列,数据从头部插入,从尾部取出。数据取出的程序与数据插 入的程序雷同。

dispatcher

Dispatcher 是对 event_base 事件循环的封装。event_base_loop() 函数在此处 运行,应用中每个 IO 线程领有一个 Dispather 实例。其中的队列 post_callbacks_ 保留来自其它线程通过 post() 办法对 Dispatcher 的操作。post() 函数中对超 时事件激活,而在超时事件中则将函数从队列中取出执行。

代码中只演示了一个队列,理论因为业务须要,可能须要多个队列以满足工作优先级的 需要。在咱们的业务程序中,敞开连贯、开释资源等操作被认为是优先级低的,咱们设 立一个独自的队列,在其它队列中的内容都解决完之后,再解决低优先级队列的工作。

thread_pool

一个简略的线程池实现。多个线程循环从工作队列中获取工作后执行。工作队列存在多 个,以实现优先级的目标。在咱们的业务中,设施身份认证蕴含很多密码学和数据库操 作,十分耗时,咱们将这个操作的优先级设置很低;绝对地,设施数据上传优先级较高。这样能够保障现有业务不因大量的高性能耗费的设施接入申请中断。

listener

关上监听端口,注册事件,触发事件后调用 accept 函数接管新连贯,接管新连贯后 调用指定函数 (handler) 解决连贯。

监听链接设置了 SO_REUSEPORT 选项,这样能够在多个线程中同时监听一个端口。

handler

连贯解决的办法。listener 接管新连贯后,实例化这个类解决新连贯。

handler 的读事件接管客户端发来的数据,放到 read_buf 中,再从 read_buf 写 到 write_buf (这里有点多余),随后注册写事件。在写事件中,将 write_buf 的 内容发送给客户端。实现 echo-server 的性能。

理论的程序中,常常须要查问某个客户端连贯,将数据从服务端被动发送给客户端,所 以咱们将 handler 放到一个哈希表中存储,并设置援用计数。但这个例子里没有这个必 要。

main

程序入口,做一些初始化工作。

总结

搞这些花里胡哨的不见得比新员工学一周 go 开发的业务程序性能高。如果你真的须要一个 与业务关系不是那么亲密,更新不频繁而且对效率要求高的程序,能够尝试应用这里介绍 的办法。

退出移动版