Libevent(4)之使用教程(3)配置事件
Author: Once Day Date: 2025年7月27日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…
漫漫长路,有人对你微笑过嘛…
本文档翻译于:Fast portable non-blocking network programming with Libevent
全系列文章可参考专栏: 十年代码训练_Once-Day的博客-CSDN博客
参考文章:
- 详解libevent网络库(一)—框架的搭建_libevent详解-CSDN博客
- 深度思考高性能网络库Libevent,从13个维度来解析Libevent到底是怎么回事 - 知乎
- 深入浅出理解libevent——2万字总结_libev 堆-CSDN博客
- Fast portable non-blocking network programming with Libevent
- libevent
- C++网络库:Libevent网络库的原理及使用方法 - 知乎
- 深入理解libevent事件库的原理与实践技巧-腾讯云开发者社区-腾讯云
文章目录
- Libevent(4)之使用教程(3)配置事件
- 6. 配置事件
- 6.1 构造事件对象
- 6.2 事件标志
- 6.3 关于事件持久化
- 6.4 将事件自身作为回调参数创建事件
- 6.5 超时时间事件
- 6.6 构建信号事件
- 6.7 使事件处于待处理与非待处理状态
- 6.8 带优先级的事件
- 6.9 检查事件状态
- 6.10 获取当前运行的事件
- 6.11 配置一次性事件
- 6.12 手动激活事件
- 6.13 优化常见超时设置
- 6.14 区分已初始化事件与清零内存
- 6.15 过时的事件操作函数
6. 配置事件
Libevent 的基本操作单元是事件。每个事件都代表一组条件,包括:
- 文件描述符准备好读取或写入。
- 文件描述符变为可读取或可写入状态(仅适用于边缘触发 IO)。
- 超时时间到期。
- 信号发生。
- 用户触发的事件。
事件具有相似的生命周期。当调用 Libevent 函数设置一个事件并将其与事件基座(event base)关联后,该事件即完成初始化。此时,可以通过 “添加” 操作使其在基座中处于待处理(pending)状态。当事件处于待处理状态时,若触发事件的条件满足(例如,其文件描述符状态改变或超时时间到期),事件会变为活跃(active)状态,并执行其(用户提供的)回调函数。如果事件配置为持久化(persistent),它会保持待处理状态;若为非持久化,则在回调函数执行后不再处于待处理状态。可以通过 “删除” 操作使待处理事件变为非待处理状态,也可以重新添加非待处理事件使其再次进入待处理状态。
6.1 构造事件对象
要创建一个新事件,请使用 event_new () 接口。
#define EV_TIMEOUT 0x01
#define EV_READ 0x02
#define EV_WRITE 0x04
#define EV_SIGNAL 0x08
#define EV_PERSIST 0x10
#define EV_ET 0x20typedef void (*event_callback_fn)(evutil_socket_t, short, void *);struct event *event_new(struct event_base *base, evutil_socket_t fd,short what, event_callback_fn cb,void *arg);void event_free(struct event *event);
event_new () 函数会尝试为事件基座(base)分配并构建一个新事件。what 参数是一组上述列出的标志(其语义将在下文描述)。如果 fd 为非负值,则它是我们将观察读写事件的文件描述符。当事件处于活跃状态时,Libevent 会调用提供的 cb 函数,并传入以下参数:文件描述符 fd、触发事件的所有标志位组合,以及构建该函数时传入的 arg 值。
若发生内部错误或参数无效,event_new () 将返回 NULL。
所有新创建的事件均处于已初始化且非待处理状态。要使事件进入待处理状态,请调用 event_add ()(下文将详细说明)。
要释放一个事件,请调用 event_free ()。对处于待处理或活跃状态的事件调用 event_free () 是安全的:这会先使事件变为非待处理和非活跃状态,再进行释放。
#include <event2/event.h>void cb_func(evutil_socket_t fd, short what, void *arg)
{const char *data = arg;printf("Got an event on socket %d:%s%s%s%s [%s]",(int) fd,(what&EV_TIMEOUT) ? " timeout" : "",(what&EV_READ) ? " read" : "",(what&EV_WRITE) ? " write" : "",(what&EV_SIGNAL) ? " signal" : "",data);
}void main_loop(evutil_socket_t fd1, evutil_socket_t fd2)
{struct event *ev1, *ev2;struct timeval five_seconds = {5,0};struct event_base *base = event_base_new();/* The caller has already set up fd1, fd2 somehow, and make themnonblocking. */ev1 = event_new(base, fd1, EV_TIMEOUT|EV_READ|EV_PERSIST, cb_func,(char*)"Reading event");ev2 = event_new(base, fd2, EV_WRITE|EV_PERSIST, cb_func,(char*)"Writing event");event_add(ev1, &five_seconds);event_add(ev2, NULL);event_base_dispatch(base);
}
上述函数定义于 <event2/event.h> 中,首次出现于 Libevent 2.0.1-alpha 版本。event_callback_fn 类型作为 typedef 首次出现于 Libevent 2.0.4-alpha 版本。
6.2 事件标志
EV_TIMEOUT:该标志表示事件在超时期满后变为活跃状态。构建事件时,EV_TIMEOUT 标志会被忽略:你可以在添加事件时设置超时时间,也可以不设置。当超时发生时,该标志会出现在回调函数的 ‘what’ 参数中。
EV_READ:该标志表示当指定的文件描述符准备好读取时,事件变为活跃状态。
EV_WRITE:该标志表示当指定的文件描述符准备好写入时,事件变为活跃状态。
EV_SIGNAL:用于实现信号检测。详见下文 “构建信号事件”。
EV_PERSIST:表示事件是持久化的。详见下文 “关于事件持久化”。
EV_ET:表示如果底层事件基座(event_base)后端支持边缘触发事件,则该事件应采用边缘触发模式。这会影响 EV_READ 和 EV_WRITE 的语义。
自 Libevent 2.0.1-alpha 版本起,多个事件可以同时针对相同条件处于待处理状态。例如,你可以有两个事件在同一文件描述符准备好读取时变为活跃状态,其回调函数的执行顺序是未定义的。
这些标志定义于 <event2/event.h> 中。除 EV_ET 于 Libevent 2.0.1-alpha 版本引入外,其余标志在 Libevent 1.0 版本之前就已存在。
6.3 关于事件持久化
默认情况下,当一个待处理事件变为活跃状态时(例如,其文件描述符准备好读写或超时期满),会在执行回调函数之前变为非待处理状态。因此,若想让事件再次进入待处理状态,可在回调函数内部再次调用 event_add ()。
然而,若事件设置了 EV_PERSIST 标志,则该事件是持久化的,这意味着即使其回调函数被激活,事件仍会保持待处理状态。若想在回调函数内部使它变为非待处理状态,可调用 event_del ()。
持久化事件的超时时间会在其回调函数每次执行时重置。因此,对于一个带有 EV_READ|EV_PERSIST 标志且超时时间为 5 秒的事件,在以下情况会变为活跃状态:
- 每当套接字准备好读取时。
- 每当距离事件上一次变为活跃状态已过去 5 秒时。
6.4 将事件自身作为回调参数创建事件
通常,你可能希望创建一个能在回调参数中接收自身的事件。不过,你不能直接将事件指针作为参数传递给 event_new (),因为此时事件尚未存在。要解决这个问题,可以使用 event_self_cbarg () 函数。
event_self_cbarg () 函数会返回一个 “魔术” 指针,当该指针作为事件回调参数传递时,会告知 event_new () 创建一个能在回调中接收自身的事件。
#include <event2/event.h>static int n_calls = 0;void cb_func(evutil_socket_t fd, short what, void *arg)
{struct event *me = arg;printf("cb_func called %d times so far.\n", ++n_calls);if (n_calls > 100)event_del(me);
}void run(struct event_base *base)
{struct timeval one_sec = { 1, 0 };struct event *ev;/* We're going to set up a repeating timer to get called called 100times. */ev = event_new(base, -1, EV_PERSIST, cb_func, event_self_cbarg());event_add(ev, &one_sec);event_base_dispatch(base);
}
该函数还可与 event_new ()、evtimer_new ()、evsignal_new ()、event_assign ()、evtimer_assign () 以及 evsignal_assign () 配合使用。但对于非事件对象,它无法作为有效的回调参数。
event_self_cbarg () 函数在 Libevent 2.1.1-alpha 版本中首次引入。
6.5 超时时间事件
为方便使用,有一组以 evtimer_
开头的宏,可替代 event_*
系列调用,用于分配和操作纯超时事件。使用这些宏除了能提高代码的清晰度外,没有其他额外优势。
#define evtimer_new(base, callback, arg) \event_new((base), -1, 0, (callback), (arg))
#define evtimer_add(ev, tv) \event_add((ev),(tv))
#define evtimer_del(ev) \event_del(ev)
#define evtimer_pending(ev, tv_out) \event_pending((ev), EV_TIMEOUT, (tv_out))
这些宏从 Libevent 0.6 版本起就已存在,不过 evtimer_new()
是在 Libevent 2.0.1-alpha 版本中才首次出现的。
6.6 构建信号事件
Libevent 也可以监听 POSIX 风格的信号。要为信号构建处理器,请使用以下方式:
#define evsignal_new(base, signum, cb, arg) \event_new(base, signum, EV_SIGNAL|EV_PERSIST, cb, arg)
其参数与 event_new 类似,不同之处在于这里提供的是信号编号而非文件描述符。
struct event *hup_event;
struct event_base *base = event_base_new();/* call sighup_function on a HUP signal */
hup_event = evsignal_new(base, SIGHUP, sighup_function, NULL);
注意,信号回调会在信号发生后的事件循环中执行,因此在回调中调用那些不允许在常规 POSIX 信号处理器中调用的函数是安全的。
警告:不要为信号事件设置超时。这可能不受支持。[待修正:情况是否属实?]
此外,还有一组便捷宏可用于处理信号事件。
#define evsignal_add(ev, tv) \event_add((ev),(tv))
#define evsignal_del(ev) \event_del(ev)
#define evsignal_pending(ev, what, tv_out) \event_pending((ev), (what), (tv_out))
evsignal_* 系列宏从 Libevent 2.0.1-alpha 版本开始出现。早期版本中它们被命名为 signal_add ()、signal_del () 等。
处理信号时的注意事项:在当前版本的 Libevent 中,对于大多数后端,同一进程中同一时间只能有一个 event_base 监听信号。如果同时向两个 event_base 添加信号事件 —— 即便信号不同 —— 也只有一个 event_base 会接收信号。kqueue 后端没有这一限制。
不使用堆分配创建事件:出于性能等方面的原因,有些人希望将事件作为更大结构的一部分进行分配。这种方式在每次使用事件时,可节省:
- 在堆上分配小对象带来的内存分配器开销。
- 解引用 struct event 指针的时间开销。
- 若事件不在缓存中时可能产生的额外缓存未命中时间开销。
但这种方法可能会破坏与其他 Libevent 版本的二进制兼容性,因为不同版本中 event 结构的大小可能不同。
这些开销通常非常小,对大多数应用程序而言无关紧要。除非确定堆分配事件会带来显著的性能损失,否则应坚持使用 event_new ()。如果未来版本的 Libevent 使用的 event 结构比当前构建时的更大,使用 event_assign () 可能会导致难以诊断的错误。
int event_assign(struct event *event, struct event_base *base,evutil_socket_t fd, short what,void (*callback)(evutil_socket_t, short, void *), void *arg);
event_assign () 的所有参数与 event_new () 相同,不同之处在于 event 参数必须指向一个未初始化的事件。成功时返回 0,发生内部错误或参数无效时返回 -1。
#include <event2/event.h>
/* Watch out! Including event_struct.h means that your code will not* be binary-compatible with future versions of Libevent. */
#include <event2/event_struct.h>
#include <stdlib.h>struct event_pair {evutil_socket_t fd;struct event read_event;struct event write_event;
};
void readcb(evutil_socket_t, short, void *);
void writecb(evutil_socket_t, short, void *);
struct event_pair *event_pair_new(struct event_base *base, evutil_socket_t fd)
{struct event_pair *p = malloc(sizeof(struct event_pair));if (!p) return NULL;p->fd = fd;event_assign(&p->read_event, base, fd, EV_READ|EV_PERSIST, readcb, p);event_assign(&p->write_event, base, fd, EV_WRITE|EV_PERSIST, writecb, p);return p;
}
也可以使用 event_assign () 初始化栈分配或静态分配的事件。
警告:切勿对已在 event base 中处于待处理状态的事件调用 event_assign ()。这样做可能会导致极难诊断的错误。如果事件已初始化且处于待处理状态,需先调用 event_del (),再调用 event_assign ()。
有一些便捷宏可用于为仅超时事件或信号事件调用 event_assign ():
#define evtimer_assign(event, base, callback, arg) \event_assign(event, base, -1, 0, callback, arg)
#define evsignal_assign(event, base, signum, callback, arg) \event_assign(event, base, signum, EV_SIGNAL|EV_PERSIST, callback, arg)
如果需要使用 event_assign () 并保持与未来 Libevent 版本的二进制兼容性,可以让 Libevent 库在运行时告知 struct event 应有的大小:
size_t event_get_struct_event_size(void);
该函数返回需要为 struct event 预留的字节数。同样,只有当确定堆分配确实是程序中的显著问题时,才应使用此函数,因为它会使代码的读写难度大幅增加。
注意,未来 event_get_struct_event_size () 返回的值可能小于 sizeof (struct event)。出现这种情况时,意味着 struct event 末尾的额外字节只是为未来 Libevent 版本预留的填充字节。
下面是与上文类似的示例,但不依赖 event_struct.h 中定义的 struct event 大小,而是使用 event_get_struct_size () 在运行时获取正确的大小。
#include <event2/event.h>
#include <stdlib.h>/* When we allocate an event_pair in memory, we'll actually allocate* more space at the end of the structure. We define some macros* to make accessing those events less error-prone. */
struct event_pair {evutil_socket_t fd;
};/* Macro: yield the struct event 'offset' bytes from the start of 'p' */
#define EVENT_AT_OFFSET(p, offset) \((struct event*) ( ((char*)(p)) + (offset) ))
/* Macro: yield the read event of an event_pair */
#define READEV_PTR(pair) \EVENT_AT_OFFSET((pair), sizeof(struct event_pair))
/* Macro: yield the write event of an event_pair */
#define WRITEEV_PTR(pair) \EVENT_AT_OFFSET((pair), \sizeof(struct event_pair)+event_get_struct_event_size())/* Macro: yield the actual size to allocate for an event_pair */
#define EVENT_PAIR_SIZE() \(sizeof(struct event_pair)+2*event_get_struct_event_size())void readcb(evutil_socket_t, short, void *);
void writecb(evutil_socket_t, short, void *);
struct event_pair *event_pair_new(struct event_base *base, evutil_socket_t fd)
{struct event_pair *p = malloc(EVENT_PAIR_SIZE());if (!p) return NULL;p->fd = fd;event_assign(READEV_PTR(p), base, fd, EV_READ|EV_PERSIST, readcb, p);event_assign(WRITEEV_PTR(p), base, fd, EV_WRITE|EV_PERSIST, writecb, p);return p;
}
event_assign () 函数定义于 <event2/event.h> 中,从 Libevent 2.0.1-alpha 版本开始存在。自 2.0.3-alpha 版本起,它返回 int 类型;在此之前,返回 void 类型。event_get_struct_event_size () 函数在 Libevent 2.0.4-alpha 版本中引入。event 结构本身定义于 <event2/event_struct.h> 中。
6.7 使事件处于待处理与非待处理状态
创建事件后,需通过添加操作使其进入待处理状态才会实际生效,这一操作可通过 event_add
完成:
int event_add(struct event *ev, const struct timeval *tv);
对非待处理状态的事件调用 event_add
,会使其在已配置的事件基座(base)中变为待处理状态。函数成功时返回 0,失败时返回 -1。若 tv
为 NULL,则添加的事件无超时设置;否则,tv
表示超时时间(单位为秒和微秒)。
若对已处于待处理状态的事件调用 event_add
,事件会保持待处理状态,并按提供的超时时间重新调度。若事件已处于待处理状态,且重新添加时 timeout
设为 NULL,则 event_add
不会产生任何效果。
注意:不要将 tv
设置为希望超时触发的具体时间。例如,在 2010 年 1 月 1 日执行 tv->tv_sec = time(NULL)+10;
,超时会等待 40 年而非 10 秒(此处为示例,实际时间差需根据具体场景计算)。
int event_del(struct event *ev);
对已初始化的事件调用 event_del
,会使其变为非待处理和非活跃状态。若事件原本就不处于待处理或活跃状态,则该操作无效果。成功时返回 0,失败时返回 -1。
注意:若在事件变为活跃状态后、其回调函数有机会执行前删除该事件,回调函数将不会被执行。
int event_remove_timer(struct event *ev);
最后,可完全移除待处理事件的超时设置,而不删除其 IO 或信号组件。若事件原本就没有待处理的超时设置,event_remove_timer()
无效果;若事件仅包含超时设置(无 IO 或信号组件),event_remove_timer()
的效果与 event_del()
相同。成功时返回 0,失败时返回 -1。
上述函数定义于 <event2/event.h>
中:event_add()
和 event_del()
从 Libevent 0.1 版本起就已存在;event_remove_timer()
则在 2.1.2-alpha 版本中新增。
6.8 带优先级的事件
当多个事件同时触发时,Libevent 并未定义它们的回调函数执行顺序。不过,你可以通过设置优先级来指定某些事件比其他事件更重要。正如前面章节所讨论的,每个 event_base 都关联有一个或多个优先级值。在将事件添加到 event_base 之前(但在事件初始化之后),你可以为其设置优先级。
int event_priority_set(struct event *event, int priority);
事件的优先级是一个介于 0 到(event_base 中的优先级数量 - 1)之间的数字。该函数成功时返回 0,失败时返回 -1。
当多个不同优先级的事件变为活跃状态时,低优先级事件不会被执行。相反,Libevent 会先执行高优先级事件,然后再次检查事件。只有当没有高优先级事件处于活跃状态时,低优先级事件才会被执行。
#include <event2/event.h>void read_cb(evutil_socket_t, short, void *);
void write_cb(evutil_socket_t, short, void *);void main_loop(evutil_socket_t fd)
{struct event *important, *unimportant;struct event_base *base;base = event_base_new();event_base_priority_init(base, 2);/* Now base has priority 0, and priority 1 */important = event_new(base, fd, EV_WRITE|EV_PERSIST, write_cb, NULL);unimportant = event_new(base, fd, EV_READ|EV_PERSIST, read_cb, NULL);event_priority_set(important, 0);event_priority_set(unimportant, 1);/* Now, whenever the fd is ready for writing, the write callback willhappen before the read callback. The read callback won't happen atall until the write callback is no longer active. */
}
如果不为事件设置优先级,其默认优先级为 event_base 中队列数量的一半。
该函数声明于 <event2/event.h>
中,从 Libevent 1.0 版本起就已存在。
6.9 检查事件状态
有时你需要判断一个事件是否已被添加,以及查看它所关联的对象。
int event_pending(const struct event *ev, short what, struct timeval *tv_out);#define event_get_signal(ev) /* ... */
evutil_socket_t event_get_fd(const struct event *ev);
struct event_base *event_get_base(const struct event *ev);
short event_get_events(const struct event *ev);
event_callback_fn event_get_callback(const struct event *ev);
void *event_get_callback_arg(const struct event *ev);
int event_get_priority(const struct event *ev);void event_get_assignment(const struct event *event,struct event_base **base_out,evutil_socket_t *fd_out,short *events_out,event_callback_fn *callback_out,void **arg_out);
event_pending
函数用于判断指定事件是否处于待处理(pending)或活跃(active)状态。如果是,且 what
参数中设置了 EV_READ、EV_WRITE、EV_SIGNAL、EV_TIMEOUT 中的任一标志,该函数会返回事件当前在待处理或活跃状态下所关联的所有标志。如果提供了 tv_out
,且 what
中设置了 EV_TIMEOUT,同时事件当前因超时而处于待处理或活跃状态,那么 tv_out
会被设置为该事件超时到期的时间。
event_get_fd()
和 event_get_signal()
函数分别返回事件所配置的文件描述符和信号编号。event_get_base()
函数返回事件所关联的 event_base。event_get_events()
函数返回事件的标志(如 EV_READ、EV_WRITE 等)。event_get_callback()
和 event_get_callback_arg()
函数分别返回回调函数和回调参数指针。event_get_priority()
函数返回事件当前被分配的优先级。
event_get_assignment()
函数会将事件的所有已分配字段复制到提供的指针中。如果某个指针为 NULL,则会忽略该字段。
#include <event2/event.h>
#include <stdio.h>/* Change the callback and callback_arg of 'ev', which must not be* pending. */
int replace_callback(struct event *ev, event_callback_fn new_callback,void *new_callback_arg)
{struct event_base *base;evutil_socket_t fd;short events;int pending;pending = event_pending(ev, EV_READ|EV_WRITE|EV_SIGNAL|EV_TIMEOUT,NULL);if (pending) {/* We want to catch this here so that we do not re-assign a* pending event. That would be very very bad. */fprintf(stderr,"Error! replace_callback called on a pending event!\n");return -1;}event_get_assignment(ev, &base, &fd, &events,NULL /* ignore old callback */ ,NULL /* ignore old callback argument */);event_assign(ev, base, fd, events, new_callback, new_callback_arg);return 0;
}
这些函数均声明于 <event2/event.h>
中。event_pending()
从 Libevent 0.1 版本起就已存在;Libevent 2.0.1-alpha 版本引入了 event_get_fd()
和 event_get_signal()
;Libevent 2.0.2-alpha 版本引入了 event_get_base()
;Libevent 2.1.2-alpha 版本新增了 event_get_priority()
;其他函数则在 Libevent 2.0.4-alpha 版本中首次出现。
6.10 获取当前运行的事件
出于调试或其他目的,你可以获取指向当前正在运行的事件的指针。
struct event *event_base_get_running_event(struct event_base *base);
需要注意的是,只有在提供的 event_base 的事件循环中调用该函数时,其行为才是确定的。在其他线程中调用该函数是不被支持的,可能会导致未定义行为。
该函数声明于 <event2/event.h>
中,在 Libevent 2.1.1-alpha 版本中引入。
6.11 配置一次性事件
如果你不需要多次添加或删除某个事件,并且该事件无需持久化,那么可以使用 event_base_once()
。
int event_base_once(struct event_base *, evutil_socket_t, short,void (*)(evutil_socket_t, short, void *), void *, const struct timeval *);
此函数的接口与 event_new()
相同,不过它不支持 EV_SIGNAL
或 EV_PERSIST
标志。被调度的事件会以默认优先级插入并执行。当回调函数最终执行完毕后,Libevent 会自行释放内部的事件结构。成功时返回 0,失败时返回 -1。
通过 event_base_once()
插入的事件无法被删除或手动激活:如果你希望能够取消某个事件,应使用常规的 event_new()
或 event_assign()
接口来创建它。
此外需要注意,在 Libevent 2.0 及之前的版本中,如果事件从未被触发,用于存储它的内部内存将永远不会被释放。从 Libevent 2.1.2-alpha 版本开始,当释放 event_base
时,这些未激活的事件也会被释放,但仍需留意:如果它们的回调参数关联了某些存储资源,除非你的程序主动跟踪并释放这些资源,否则它们不会自动被释放。
6.12 手动激活事件
极少数情况下,你可能希望在事件条件未触发时强制使其变为活跃状态。
void event_active(struct event *ev, int what, short ncalls);
event_active()
函数可使事件 ev
以指定标志 what
(由 EV_READ、EV_WRITE 和 EV_TIMEOUT 组合而成)变为活跃状态。该事件无需事先处于待处理状态,激活操作也不会使其进入待处理状态。
警告:对同一事件递归调用 event_active()
可能导致资源耗尽。以下代码片段展示了错误使用 event_active()
的情况:
struct event *ev;static void cb(int sock, short which, void *arg) {/* Whoops: Calling event_active on the same event unconditionallyfrom within its callback means that no other events might not getrun! */event_active(ev, EV_WRITE, 0);
}int main(int argc, char **argv) {struct event_base *base = event_base_new();ev = event_new(base, -1, EV_PERSIST | EV_READ, cb, NULL);event_add(ev, NULL);event_active(ev, EV_WRITE, 0);event_base_loop(base, 0);return 0;
}
这种实现会导致事件循环仅执行一次,随后无限调用回调函数 cb
,形成死循环。
// Example: Alternative solution to the above problem using timersstruct event *ev;
struct timeval tv;static void cb(int sock, short which, void *arg) {if (!evtimer_pending(ev, NULL)) {event_del(ev);evtimer_add(ev, &tv);}
}int main(int argc, char **argv) {struct event_base *base = event_base_new();tv.tv_sec = 0;tv.tv_usec = 0;ev = evtimer_new(base, cb, NULL);evtimer_add(ev, &tv);event_base_loop(base, 0);return 0;
}// Example: Alternative solution to the above problem using event_config_set_max_dispatch_interval()
struct event *ev;static void cb(int sock, short which, void *arg) {event_active(ev, EV_WRITE, 0);
}int main(int argc, char **argv) {struct event_config *cfg = event_config_new();/* Run at most 16 callbacks before checking for other events. */event_config_set_max_dispatch_interval(cfg, NULL, 16, 0);struct event_base *base = event_base_new_with_config(cfg);ev = event_new(base, -1, EV_PERSIST | EV_READ, cb, NULL);event_add(ev, NULL);event_active(ev, EV_WRITE, 0);event_base_loop(base, 0);return 0;
}
event_active()
函数定义于 <event2/event.h>
中,自 Libevent 0.3 版本起就已存在。
6.13 优化常见超时设置
当前版本的 Libevent 采用二叉堆算法来跟踪待处理事件的超时设置。对于添加和删除每个事件的超时设置,二叉堆的性能为 O (lg n) 级别。如果添加的事件超时值是随机分布的,这种算法是最优的;但如果有大量事件具有相同的超时值,它就不是最优选择了。
例如,假设有一万个事件,每个事件在添加后 5 秒触发超时。在这种情况下,使用双向链表队列实现可以使每个超时操作的性能达到 O(1)。
当然,你不会希望对所有超时值都使用队列,因为队列仅在超时值固定时更快。如果某些超时值大致呈随机分布,那么将这些超时值添加到队列中会花费 O(n) 的时间,这比二叉堆要慢得多。
Libevent 允许你将部分超时设置放入队列,其余的放入二叉堆,以此解决上述问题。具体做法是:向 Libevent 请求一个特殊的 “通用超时” timeval,然后使用该 timeval 来添加具有相同超时设置的事件。如果有大量事件使用单一的通用超时设置,这种优化可以提升超时处理的性能。
const struct timeval *event_base_init_common_timeout(struct event_base *base, const struct timeval *duration);
该函数接收 event_base 和要初始化的通用超时持续时间作为参数,返回一个指向特殊 struct timeval 的指针。使用这个特殊的 timeval 添加事件时,事件会被放入 O(1) 级别的队列,而非 O(lgn) 级别的堆。你可以在代码中自由复制或赋值这个特殊的 timeval,但它仅对用于构建它的特定 base 有效。不要依赖其实际内容:Libevent 会利用这些内容来确定使用哪个队列。
#include <event2/event.h>
#include <string.h>/* We're going to create a very large number of events on a given base,* nearly all of which have a ten-second timeout. If initialize_timeout* is called, we'll tell Libevent to add the ten-second ones to an O(1)* queue. */
struct timeval ten_seconds = { 10, 0 };void initialize_timeout(struct event_base *base)
{struct timeval tv_in = { 10, 0 };const struct timeval *tv_out;tv_out = event_base_init_common_timeout(base, &tv_in);memcpy(&ten_seconds, tv_out, sizeof(struct timeval));
}int my_event_add(struct event *ev, const struct timeval *tv)
{/* Note that ev must have the same event_base that we passed toinitialize_timeout */if (tv && tv->tv_sec == 10 && tv->tv_usec == 0)return event_add(ev, &ten_seconds);elsereturn event_add(ev, tv);
}
与所有优化函数一样,除非确定它对性能有显著影响,否则应避免使用通用超时功能。
此功能在 Libevent 2.0.4-alpha 版本中引入。
6.14 区分已初始化事件与清零内存
Libevent 提供了一些函数,可用于区分已初始化的事件和被清零的内存(例如,通过 calloc() 分配或用 memset()、bzero() 清零的内存)。
int event_initialized(const struct event *ev);#define evsignal_initialized(ev) event_initialized(ev)
#define evtimer_initialized(ev) event_initialized(ev)
这些函数无法可靠地区分已初始化事件和未初始化内存块。只有当确定相关内存要么已清零,要么已初始化为事件时,才应使用它们。通常,除非有非常特定的应用场景,否则无需使用这些函数。event_new () 返回的事件始终是已初始化的。
#include <event2/event.h>
#include <stdlib.h>struct reader {evutil_socket_t fd;
};#define READER_ACTUAL_SIZE() \(sizeof(struct reader) + \event_get_struct_event_size())#define READER_EVENT_PTR(r) \((struct event *) (((char*)(r))+sizeof(struct reader)))struct reader *allocate_reader(evutil_socket_t fd)
{struct reader *r = calloc(1, READER_ACTUAL_SIZE());if (r)r->fd = fd;return r;
}void readcb(evutil_socket_t, short, void *);
int add_reader(struct reader *r, struct event_base *b)
{struct event *ev = READER_EVENT_PTR(r);if (!event_initialized(ev))event_assign(ev, b, r->fd, EV_READ, readcb, r);return event_add(ev, NULL);
}
event_initialized () 函数从 Libevent 0.3 版本起就已存在。
6.15 过时的事件操作函数
Libevent 2.0 之前的版本没有 event_assign () 或 event_new (),而是使用 event_set () 函数将事件与 “当前” 基座(base)关联。如果有多个基座,需要记得在之后调用 event_base_set (),以确保事件关联到实际想要使用的基座。
void event_set(struct event *event, evutil_socket_t fd, short what,void(*callback)(evutil_socket_t, short, void *), void *arg);
int event_base_set(struct event_base *base, struct event *event);
event_set () 函数类似于 event_assign (),不同之处在于它使用当前基座。event_base_set () 函数用于更改事件所关联的基座。
event_set () 有一些变体,方便处理定时器和信号:evtimer_set () 大致对应于 evtimer_assign (),evsignal_set () 大致对应于 evsignal_assign ()。
Libevent 2.0 之前的版本中,基于信号的 event_set() 变体使用 signal_
作为前缀,而非 evsignal_
(即有 signal_set()、signal_add()、signal_del()、signal_pending() 和 signal_initialized())。真正古老的 Libevent 版本(0.6 之前)使用 timeout_
而非 evtimer_
,因此,如果进行代码考古,可能会看到 timeout_add()、timeout_del()、timeout_initialized()、timeout_set()、timeout_pending () 等函数。
Libevent 2.0 之前的版本没有 event_get_fd () 和 event_get_signal () 函数,而是使用两个宏:EVENT_FD () 和 EVENT_SIGNAL ()。这些宏直接访问事件结构的内容,因此破坏了不同版本间的二进制兼容性;在 2.0 及之后的版本中,它们只是 event_get_fd () 和 event_get_signal () 的别名。
由于 Libevent 2.0 之前的版本不支持锁定,从运行基座的线程外部调用任何更改事件相对于基座状态的函数都是不安全的,包括 event_add ()、event_del ()、event_active () 和 event_base_once ()。
还有一个 event_once () 函数,作用类似于 event_base_once (),但使用当前基座。
在 Libevent 2.0 之前,EV_PERSIST 标志与超时的交互不够合理。当时,EV_PERSIST 标志不会在事件激活时重置超时,这与现在的行为不同。
Libevent 2.0 之前的版本不支持同时插入多个具有相同文件描述符和相同读写标志的事件。换句话说,每个文件描述符每次只能有一个事件等待读取,也只能有一个事件等待写入。
Once Day
也信美人终作土,不堪幽梦太匆匆......
如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!
(。◕‿◕。)感谢您的阅读与支持~~~