由于老大在新项目中使用redis的事件库代替了libevent,我也趁着机会读了一遍redis的事件库代码,第一次读到“优美,让人愉快”的代码,加之用xmind制作的类图非常帅,所以留文纪念。

         Redis的事件库主要集中在ae.h和ae.c中,此外还有ae_epoll.c等底层实现文件,根据系统可选择事件库的实现方式,典型的讲上层实现与底层实现分离的结构。

         ae.h是事件库函数的定义与使用的结构体,关于结构体的相互联系可参见类图。

         除了事件库,Redis还封装了malloc等内存管理函数,为标准函数增加了一个记录占用内存量的部分,如果想独立使用redis的事件库,只要把ae.c中的zmalloc全部替换成标准malloc即可。

         下面开始介绍ae.c,一段典型的ae事件库如下图所示:

int main()
{
    aeEventLoop el;
    el = aeCreateEventLoop(1024);
    aeCreateFileEvent(el, fd, mask, proc, clientData);
    aeCreateTimeEvent(el, milliseconds, proc, clientData, finalizerProc);
    aeMain(el);
}

         aeCreateEventLoop用于对ae事件库进行初始化,然后通过aeCreateFileEvent和aeCreateTImeEvent可以添加需要监控的fd读写事件或者定时事件,通过类图,可以看到对于fd读写时间有两个钩子,一个是读事件的钩子,一个是写时间的钩子;对于定时事件也有两个钩子,一个是定时时间到达时的钩子,一个是delete定时事件时执行的钩子。

         创建完我们需要的事件后,调用aeMain进入事件驱动。aeMain中,判断几个执行变量即开始死循环调用aeProcessEvents处理事件。

         aeProcessEvents首先需要从时间事件中寻找最近要执行的定时事件时间,此时间与当前时间的间隔即是调用aeApiPoll的超时时间,避免因epoll超时,影响定时事件执行。aeApiPoll会将需要处理的fd事件,放入fired队列,因此调用aeApiPoll之后,是通过遍历fired队列来执行fd事件的。fd事件处理结束,调用processTimeEvents处理定时事件,processTimeEvents每执行一个定时事件都要重新遍历定时事件队列,因为每个定时事件的执行,都会导致当前时间改变,从而有新的事件需要执行,但是processTimeEvents不会处理新加入的时间事件,以免该循环永不退出(但是目前还是有可能因为定时事件执行时间过长而导致循环永不退出)。到此整个ae事件驱动的核心就结束了。

         需要注意的是,定时事件的钩子函数需要返回定时事件的时间间隔,或者返回AE_NOMORE表示删除该定时事件,实际上这也允许了我们动态调整定时事件的时间间隔。

         Ae时间驱动,可以优化的地方很多,在代码注释中,开发人员解释道,因为redis对事件驱动的使用并不复杂,因此没有进行更多的优化,例如ae的定时事件队列实现上就是一个队列,因此寻找最近要执行的定时事件需要O(n)的复杂度,因为这个问题,执行定时事件时,也需要不断重复遍历定时队列,查找要执行的事件,我想最普遍的想法就是将这个队列实现为堆,或者二叉树,但是redis里建议实现为跳表,跳表的算法我看过一次,没发现特殊的性能优势,有时间还要研究研究。