之前说过了signal的集成处理,我们二补的时候已经分析过timer的集成,现在简单看一下libevent的方法。
集成到事件主循环
之前我们分析过,select和epoll等都允许程序制定一个最大等待时间timeout,即使I/O没有发生,也能保证timeout后返回。
我们把timer事件的最小超时时间来设置timeout,这样IO返回的同时,timer就绪事件也被激活,这样就可以将timer和IO统一管理。
这个是在事件主循环里面设置的,所以我们从loop中找到timer的处理部分,来看一下代码的实现思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) { // 根据Timer事件计算evsel->dispatch的最大等待时间 timeout_next(base, &tv_p); } else { // 如果还有活动事件,就不要等待,让evsel->dispatch立即返回 evutil_timerclear(&tv); } // ... // 调用select() or epoll_wait() 等待就绪I/O事件 res = evsel->dispatch(base, evbase, tv_p); // ... // 处理超时事件,将超时事件插入到激活链表中 timeout_process(base); |
里面的timeout_next函数是根据堆上的最小超时时间和当前时间来计算等待时间的。来看一下这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | static int timeout_next(struct event_base *base, struct timeval **tv_p) { struct timeval now; struct event *ev; struct timeval *tv = *tv_p; // 堆的首元素具有最小的超时值 if ((ev = min_heap_top(&base->timeheap)) == NULL) { // 如果没有超时事件,将等待时间设置为NULL,主循环一直阻塞直到有I/O事件发生 *tv_p = NULL; return (0); } // 取得当前时间 gettime(base, &now); // 如果超时时间<=当前值,不能等待,需要立即返回 //插一句话,这个<=真是把我秀到了,仔细分析后,我断定这是个宏定义,表示小于等于operator if (evutil_timercmp(&ev->ev_timeout, &now, <=)) { evutil_timerclear(tv); return (0); } // 计算等待的时间=当前时间-最小的超时时间 evutil_timersub(&ev->ev_timeout, &now, tv); return (0); } |
可以看到,可以直接通过堆首元素来判断是否有超时事件,如果有,则通过当前时间和最小超时时间计算等待时间。
min_heap
timer是通过小根堆来管理事件的,节点值就是事件的超时时间。堆我们比较熟悉,这个最小堆就是堆顶是最小超时时间,插入和删除都是lgn级别的,获取最小就是堆顶,O1就可以。
libevent的实现方式也有意思,我先为新元素预留一个hole,然后把他和父节点比较,不断往上回溯,直到找到自己的位置,然后给此hole赋值。这样的话就可以少一次赋值操作。(其实就是直接将调整的节点放到hole,本身就成了一个hole,这样最后在hole里填新元素就可以了,整体下来少了一次赋值)
总结:timer的集成比较简单,将主循环IO里的时间和最小堆的超时时间绑定即可。当timeout==mintimer时,IO返回时,timer就绪事件便可以处理。