libevent源码分析(四)——timer集成

之前说过了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就绪事件便可以处理。