Libevent允许创建一个超时event,使用evtimer_new宏。
[cpp] view plain copy //event.h文件 #define evtimer_new(b, cb, arg) event_new((b), -1, 0, (cb), (arg))从宏的实现来看,它一样是用到了一般的event_new,并且不使用任何的文件描述符。从超时event宏的实现来看,无论是evtimer创建的event还是一般event_new创建的event,都能使得Libevent进行超时监听。其实,使得Libevent对一个event进行超时监听的原因是:在调用event_add的时候,第二参数不能为NULL,要设置一个超时值。如果为NULL,那么Libevent将不会为这个event监听超时。下文统一称设置了超时值的event为超时event。
Libevent运行用户同时监听多个超时event,那么就必须要对这个超时值进行管理。Libevent提供了小根堆和通用超时(common timeout)这两种管理方式。下文为了叙述方便,就假定使用的是小根堆。
下面来看一下超时event的工作流程。
对于同一个event,如果是IO event或者信号event,那么将无法多次添加。但如果是一个超时event,那么是可以多次添加的。并且对应超时值会使用最后添加时指明的那个,之前的统统不要,即替换掉之前的超时值。
代码中出现了多次使用了notify变量。这主要是用在:次线程在执行这个函数,而主线程在执行event_base_dispatch。前面说到Libevent能对超时event进行监听的原理是:多路IO复用函数有一个超时参数。在次线程添加的event的超时值更小,又或者替换了之前最小的超时值。在这种情况下,都是要通知主线程,告诉主线程,最小超时值已经变了。关于通知主线程evthread_notify_base,可以参考博文《evthread_notify_base通知主线程》。
代码中的第三个判断体中用到了ev->ev_io_timeout。但event结构体中并没有该变量。其实,ev_io_timeout是一个宏定义。 [cpp] view plain copy //event-internal.h文件 #define ev_io_timeout _ev.ev_io.ev_timeout要注意的一点是,在调用event_add时设定的超时值是一个时间段(可以认为隔多长时间就触发一次),相对于现在,即调用event_add的时间,而不是调用event_base_dispatch的时间。
现在来看一下event_base_loop函数,看其是怎么处理超时event的。
[cpp] view plain copy //event.c文件 int event_base_loop(struct event_base *base, int flags) { const struct eventop *evsel = base->evsel; struct timeval tv; struct timeval *tv_p; int res, done, retval = 0; EVBASE_ACQUIRE_LOCK(base, th_base_lock); base->running_loop = 1; done = 0; while (!done) { tv_p = &tv; if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) { // 根据Timer事件计算evsel->dispatch的最大等待时间(超时值最小) timeout_next(base, &tv_p); } else { //不进行等待 //把等待时间置为0,即可不进行等待,马上触发事件 evutil_timerclear(&tv); } res = evsel->dispatch(base, tv_p); //处理超时事件,将超时事件插入到激活链表中 timeout_process(base); if (N_ACTIVE_CALLBACKS(base)) { int n = event_process_active(base); } } done: base->running_loop = 0; EVBASE_RELEASE_LOCK(base, th_base_lock); return (retval); } //选出超时值最小的那个 static int timeout_next(struct event_base *base, struct timeval **tv_p) { /* Caller must hold th_base_lock */ struct timeval now; struct event *ev; struct timeval *tv = *tv_p; int res = 0; // 堆的首元素具有最小的超时值,这个是小根堆的性质。 ev = min_heap_top(&base->timeheap); //堆中没有元素 if (ev == NULL) { *tv_p = NULL; goto out; } //获取当然时间 if (gettime(base, &now) == -1) { res = -1; goto out; } // 如果超时时间<=当前时间,不能等待,需要立即返回 // 因为ev_timeout这个时间是由event_add调用时的绝对时间 + 相对时间。所以ev_timeout是 // 绝对时间。可能在调用event_add之后,过了一段时间才调用event_base_diapatch,所以 // 现在可能都过了用户设置的超时时间。 if (evutil_timercmp(&ev->ev_timeout, &now, <=)) { evutil_timerclear(tv); //清零,这样可以让dispatcht不会等待,马上返回 goto out; } // 计算等待的时间=当前时间-最小的超时时间 evutil_timersub(&ev->ev_timeout, &now, tv); out: return (res); }上面代码的流程是:计算出本次调用多路IO复用函数的等待时间,然后调用多路IO复用函数中等待超时。
上面代码中的timeout_process函数就是处理超了时的event。
[cpp] view plain copy //event.c文件 //把超时了的event,放到激活队列中。并且,其激活原因设置为EV_TIMEOUT static void timeout_process(struct event_base *base) { /* Caller must hold lock. */ struct timeval now; struct event *ev; if (min_heap_empty(&base->timeheap)) { return; } gettime(base, &now); //遍历小根堆的元素。之所以不是只取堆顶那一个元素,是因为当主线程调用多路IO复用函数 //进入等待时,次线程可能添加了多个超时值更小的event while ((ev = min_heap_top(&base->timeheap))) { //ev->ev_timeout存的是绝对时间 //超时时间比此刻时间大,说明该event还没超时。那么余下的小根堆元素更不用检查了。 if (evutil_timercmp(&ev->ev_timeout, &now, >)) break; //下面说到的del是等同于调用event_del.把event从这个event_base中(所有的队列都) //删除。event_base不再监听之。 //这里是timeout_process函数。所以对于有超时的event,才会被del掉。 //对于有EV_PERSIST选项的event,在处理激活event的时候,会再次添加进event_base的。 //这样做的一个好处就是,再次添加的时候,又可以重新计算该event的超时时间(绝对时间)。 event_del_internal(ev); //把这个event加入到event_base的激活队列中。 //event_base的激活队列又有该event了。所以如果该event是EV_PERSIST的,是可以 //再次添加进该event_base的 event_active_nolock(ev, EV_TIMEOUT, 1); } }当从多路IO复用函数返回时,就检查时间小根堆,看有多少个event已经超时了。如果超时了,那就把这个event加入到event_base的激活队列中。并且把这个超时del(删除)掉,这主要是用于非PERSIST 超时event的。删除一个event的具体操作可以查看这里。
把一个event添加进激活队列后的工作流程可以参考《Libevent工作流程探究》一文。
这段代码的处理流程是:如果用户指定了EV_PERSIST,那么在event_assign中就记录下来。在event_process_active_single_queue函数中会针对永久event进行调用event_persist_closure函数对之进行处理。在event_persist_closure函数中,如果是一般的永久event,那么就直接调用该event的回调函数。如果是超时永久event,那么就需要再次计算新的超时时间,并将这个event再次插入到event_base中。
这段代码也指明了,如果一个event因可读而被激活,那么其超时时间就要重新计算。而不是之前的那个了。也就是说,如果一个event设置了3秒的超时,但1秒后就可读了,那么下一个超时值,就要重新计算设置,而不是2秒后。
从前面的源码分析也可以得到:如果一个event监听可读的同时也设置了超时值,并且一直没有数据可读,最后超时了,那么这个event将会被删除掉,不会再等。