多线程使用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开发的业务程序性能高。如果你真的需要一个 与业务关系不是那么密切,更新不频繁而且对效率要求高的程序,可以尝试使用这里介绍 的方法。

49 声望
2 粉丝
0 条评论
推荐阅读
以实例说明 OAuth2
OAuth2 是互联网中广泛使用的授权标准,常用于实现单点登录、第三方授权。虽然当前有 更完善的流程,但国内主要还是使用OAuth标准。国内一些服务商的OAuth是自己修改过的, 没有依照标准文档实现,经常发生标准库...

zltl阅读 1.2k

封面图
Redis 发布订阅模式:原理拆解并实现一个消息队列
“65 哥,如果你交了个漂亮小姐姐做女朋友,你会通过什么方式将这个消息广而告之给你的微信好友?““那不得拍点女朋友的美照 + 亲密照弄一个九宫格图文消息在朋友圈发布大肆宣传,暴击单身狗。”像这种 65 哥通过朋...

码哥字节6阅读 1.4k

封面图
C 程序眼中的 Unicode
去年写了一篇文章「在 C 程序中处理 UTF-8 字符串」,介绍了如何使用 GLib 提供的 UTF-8 字符串处理函数来实现基本的 UTF-8 文本处理。不过,GLib 是一个功能比较全面的 C 程序库,C 字符串处理仅仅是它的一个很...

garfileo3阅读 5.7k评论 5

工具篇:iTerm与Zsh
iTerm2支持许多的主题配色,可以自己定义,也可以参考网上现成的主题配色。我个人比较喜欢draculatheme配色。支持item,vim,phpstorm , 下方存在主题官网路径,按照教程安装即可。

super白4阅读 4.7k

万字避坑指南!C++的缺陷与思考(上)
导语 | 本文主要总结了本人在C++开发过程中对一些奇怪、复杂的语法的理解和思考,同时作为C++开发的避坑指南。前言C++是一门古老的语言,但仍然在不间断更新中,不断引用新特性。但与此同时C++又甩不掉巨大的历史...

腾讯云开发者6阅读 521

麒麟操作系统 (kylinos) 从入门到精通 - 常用软件安装 - 第三篇 常用软件安装(windows下的习惯)
本篇内容大部分从应用商店进行安装,部分通过官网下载,少部分通过命令行安装。1.原生应用1.1钉钉1.2飞书1.3 蓝信1.4 腾讯文档1.5 金山文档1.6 搜狗输入法(拼音)1.7 五笔输入法1.8 libreoffice官方也带了WPS,...

码上世界3阅读 7.4k评论 17

封面图
深入剖析容器网络和 iptables
Docker 能为我们提供很强大和灵活的网络能力,很大程度上要归功于与 iptables 的结合。在使用时,你可能没有太关注到 iptables 的作用,这是因为 Docker 已经帮我们自动完成了相关的配置。

张晋涛3阅读 1.3k

封面图
49 声望
2 粉丝
宣传栏