目录
以下源码均基于libevent-2.0.21-stable。
在前面的文章中,把Libevent对三种不同类型event的创建、添加、激活到处理过程基本上就讲的差不多了,接下来就讲一下其它的问题。目前版本Libevent是支持多线程的,既然是多线程,就会出现一系列与线程安全相关的问题,不过在Libevent中这一点似乎体现的很明确了:在所有需要加锁的地方都加了锁,在所有需要检查是否加锁的地方都进行了检查,这也是为什么在许多函数的开头都使用了EVBASE_ACQUIRE_LOCK宏来加锁,用EVENT_BASE_ASSERT_LOCKED来确保持有锁等等。
尽管通过这样的方式从最大程度上保证了线程安全,但是也有一些地方是需要进行特殊处理的。举个例子,当调用后端的dispatch函数进行事件监听时,如果dispatch设置了超时阻塞,主线程就会阻塞在dispatch直到超时或者有事件发生,那么,如果此时次线程需要添加一个event,就必须得等到dispatch返回后才能真正把新的event添加监听,这显然是不行的,同样,删除一个event、手动激活一个event、中途需要退出事件主循环等等情况都需要进行考虑。因此,这里就需要解决一个问题:如何在事件主循环阻塞的时候能够及时响应上述行为?
唤醒事件机制
之所以主线程不能立刻响应,是因为主线程正在阻塞与dispatch监听上,由于没有任何事件发生因此dispatch会阻塞到设置的超时后才返回,为了能让dispatch在需要返回的时候立刻返回,Libevent采用了和处理信号event相同的方式:定义一个内部事件专门用来唤醒主线程,这个内部事件是一个读监听事件,通过event_add添加后在主循环中进行监听。当dispatch由于无事件发生而处于阻塞,而此时又恰好有事件的添加、删除和手动激活等行为,就可以直接在这些行为执行过程中向内部事件对应的文件描述符写入数据,这样内部事件监听的读事件就会被触发,dispatch就会立刻返回, 这样就能及时响应前面所述行为了。
下面就来说一下整个唤醒机制的流程:(可以先参考Libevent处理信号event的方式)
唤醒机制相关结构体
与事件event一样,event_base中也会存在这样一个内部事件用来进行唤醒,以及一些其他的数据成员,如下所示:
struct event_base {
......
/* Notify main thread to wake up break, etc. */
/** True if the base already has a pending notify, and we don't need
* to add any more. */
int is_notify_pending;//如果为1表示当前可以唤醒主线程,否则不能唤醒主线程
/** A socketpair used by some th_notify functions to wake up the main
* thread. */
evutil_socket_t th_notify_fd[2];//一端读、一端写,用来触发唤醒事件
/** An event used by some th_notify functions to wake up the main
* thread. */
struct event th_notify;//唤醒event,被添加到监听集合中的对象
/** A function used to wake up the main thread from another thread. */
int (*th_notify_fn)(struct event_base *base);//执行唤醒操作的函数(不是唤醒event的回调函数)
};
关于这4个成员变量,先来说一下整个唤醒的流程:主线程会监听一个唤醒event,当这个唤醒event激活时dispatch就会从阻塞中返回。而为了触发这个唤醒event,就必须要触发它的“可读事件”,因此就在需要唤醒主线程的时候,通过某种通信机制,向唤醒event对应的文件描述符中写入数据,这样就能触发唤醒event的可读事件,dispatch也能够提前返回了。
而在另一方面,多线程的情况下如果有多个线程在同一时间内都准备唤醒主线程,那么实际上只用唤醒一次即可,即使在dispatch返回前同时执行了添加、删除、手动激活等多个操作,只要他们中的一个操作唤醒了主线程,那么当主线程返回时都会响应所有操作。如果每个操作都去执行唤醒,当然结果依然是正确的,但是这样一来就白白降低了效率。event_base中的is_notify_pending的作用就是这样,当一个操作执行了唤醒,那么is_notify_pending就被置为1,在主线程被唤醒前,如果其他操作还试图去唤醒主线程,发现is_notify_pending为1,那么就说明“已经有其他线程帮忙唤醒,就不需要自己动手了”,当然,主线程唤醒之后就需要把is_notify_pending重置为0;
th_notify_fd是一个文件描述符数组,包含了两个文件描述符,也就类似于信号event处理过程中使用到的socket pair,通过这两个文件描述符之间的读写,来触发唤醒event;
th_notify是一个event,它就是前面所说的被添加到监听集合中的“唤醒event”;
th_notify_fn是一个函数指针,指向一个函数,这个函数是用来唤醒主线程的函数,用来向th_notify_fd中的写端写入数据,从而来触发唤醒event。
整个唤醒的流程,实际上就是对以上四个成员进行操作,现在就来分析一下整个流程:
创建唤醒event
显然,唤醒event是一个内部事件,用户自己是无法去创建、添加这样一个事件的。Libevent中,内部事件的创建和添加都是通过内部函数来进行的。当创建一个event_base的时候,在event_base_new_with_config函数中就会创建这样一个内部事件:
struct event_base *
event_base_new_with_config(const struct event_config *cfg)
{
......
#ifndef _EVENT_DISABLE_THREAD_SUPPORT //如果支持多线程,就给base分配锁和条件变量
if (EVTHREAD_LOCKING_ENABLED() && //如果启用了线程锁,并且没有设置cfg或者flags中没有设置无锁
(!cfg || !(cfg->flags & EVENT_BASE_FLAG_NOLOCK))) {
int r;
......
r = evthread_make_base_notifiable(base);//为base的th_notify_fd注册了一对文件描述符&