libevent源码深度剖析九

libevent源码深度剖析九


——集成定时器事件
张亮

现在再来详细分析libevent中I/O事件和Timer事件的集成,与Signal相比,Timer事件的集成会直观和简单很多。Libevent对堆的调整操作做了一些优化,本节还会描述这些优化方法。

1 集成到事件主循环

因为系统的I/O机制像select()和epoll_wait()都允许程序制定一个最大等待时间(也称为最大超时时间)timeout,即使没有I/O事件发生,它们也保证能在timeout时间内返回。

那么根据所有Timer事件的最小超时时间来设置系统I/O的timeout时间;当系统I/O返回时,再激活所有就绪的Timer事件就可以了,这样就能将Timer事件完美的融合到系统的I/O机制中了。

具体的代码在源文件event.c的event_base_loop()中,现在就对比代码来看看这一处理方法:

[cpp] view plaincopy

  1. if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {

  2. // 根据Timer事件计算evsel->dispatch的最大等待时间

  3. timeout_next(base, &tv_p);

  4. } else {

  5. // 如果还有活动事件,就不要等待,让evsel->dispatch立即返回

  6. evutil_timerclear(&tv);

  7. }

  8. // ...

  9. // 调用select() or epoll_wait() 等待就绪I/O事件

  10. res = evsel->dispatch(base, evbase, tv_p);

  11. // ...

  12. // 处理超时事件,将超时事件插入到激活链表中

  13. timeout_process(base);

timeout_next()函数根据堆中具有最小超时值的事件和当前时间来计算等待时间,下面看看代码:

[cpp] view plaincopy

  1. static int timeout_next(struct event_base *base, struct timeval **tv_p)

  2. {

  3. struct timeval now;

  4. struct event *ev;

  5. struct timeval *tv = *tv_p;

  6. // 堆的首元素具有最小的超时值

  7. if ((ev = min_heap_top(&base->timeheap)) == NULL) {

  8. // 如果没有定时事件,将等待时间设置为NULL,表示一直阻塞直到有I/O事件发生

  9. *tv_p = NULL;

  10. return (0);

  11. }

  12. // 取得当前时间

  13. gettime(base, &now);

  14. // 如果超时时间<=当前值,不能等待,需要立即返回

  15. if (evutil_timercmp(&ev->ev_timeout, &now, <=)) {

  16. evutil_timerclear(tv);

  17. return (0);

  18. }

  19. // 计算等待的时间=当前时间-最小的超时时间

  20. evutil_timersub(&ev->ev_timeout, &now, tv);

  21. return (0);

  22. }

2 Timer小根堆

Libevent使用堆来管理Timer事件,其key值就是事件的超时时间,源代码位于文件min_heap.h中。

所有的数据结构书中都有关于堆的详细介绍,向堆中插入、删除元素时间复杂度都是O(lgN),N为堆中元素的个数,而获取最小key值(小根堆)的复杂度为O(1)。堆是一个完全二叉树,基本存储方式是一个数组。

     
Libevent实现的堆还是比较轻巧的,虽然我不喜欢这种编码方式(搞一些复杂的表达式)。轻巧到什么地方呢,就以插入元素为例,来对比说明,下面伪代码中的size表示当前堆的元素个数:

典型的代码逻辑如下:

[c-sharp] view plaincopy

  1. Heap[size++] = new; // 先放到数组末尾,元素个数+1

  2. // 下面就是shift_up()的代码逻辑,不断的将new向上调整

  3. _child = size;

  4. while(_child>0) // 循环

  5. {

  6. _parent = (_child-1)/2; // 计算parent

  7. if(Heap[_parent].key < Heap[_child].key)

  8. break; // 调整结束,跳出循环

  9. swap(_parent, _child); // 交换parent和child

  10. }

而libevent的heap代码对这一过程做了优化,在插入新元素时,只是为新元素预留了一个位置hole(初始时hole位于数组尾部),但并不立刻
将新元素插入到hole上,而是不断向上调整hole的值,将父节点向下调整,最后确认hole就是新元素的所在位置时,才会真正的将新元素插入到
hole上,因此在调整过程中就比上面的代码少了一次赋值的操作,代码逻辑是:
    
下面就是shift_up()的代码逻辑,不断的将new的“预留位置”向上调整

[cpp] view plaincopy

  1. // 下面就是shift_up()的代码逻辑,不断的将new的“预留位置”向上调整

  2. _hole = size; // _hole就是为new预留的位置,但并不立刻将new放上

  3. while(_hole>0) // 循环

  4. {

  5. _parent = (_hole-1)/2; // 计算parent

  6. if(Heap[_parent].key < new.key)

  7. break; // 调整结束,跳出循环

  8. Heap[_hole] = Heap[_parent]; // 将parent向下调整

  9. _hole = _parent; // 将_hole调整到_parent

  10. }

  11. Heap[_hole] = new; // 调整结束,将new插入到_hole指示的位置

  12. size++; // 元素个数+1

由于每次调整都少做一次赋值操作,在调整路径比较长时,调整效率会比第一种有所提高。libevent中的min_heap_shift_up_()函数就是上面逻辑的具体实现,对应的向下调整函数是min_heap_shift_down_()。

举个例子,向一个小根堆3, 5, 8, 7, 12中插入新元素2,使用第一中典型的代码逻辑,其调整过程如下图所示:
 

使用libevent中的堆调整逻辑,调整过程如下图所示:
 

对于删除和元素修改操作,也遵从相同的逻辑,就不再罗嗦了。

3 小节

通过设置系统I/O机制的wait时间,从而简捷的集成Timer事件;主要分析了libevent对堆调整操作的优化。

时间: 2024-10-16 00:44:33

libevent源码深度剖析九的相关文章

libevent源码深度剖析

libevent 源码深度剖析,from: blog.csdn.net/sparkliang/article/category/660506 http://download.csdn.net/detail/sparkliang/2001038#comment http://libevent.org/

libevent 源码深度剖析十三

libevent 源码深度剖析十三 -- libevent 信号处理注意点 前面讲到了 libevent 实现多线程的方法,然而在多线程的环境中注册信号事件,还是有一些情况需要小心处理,那就是不能在多个 libevent 实例上注册信号事件.依然冠名追加到 libevent 系列. 以 2 个线程为例,做简单的场景分析. 1 首先是创建并初始化线程 1 的 libevent 实例 base1 ,线程 1 的 libevent 实例 base2 : 2 在 base1 上注册 SIGALRM 信号

libevent源码深度剖析二

libevent源码深度剖析二 --Reactor模式 张亮 前面讲到,整个libevent本身就是一个Reactor,因此本节将专门对Reactor模式进行必要的介绍,并列出libevnet中的几个重要组件和Reactor的对应关系,在后面的章节中可能还会提到本节介绍的基本概念. 1 Reactor的事件处理机制 首先来回想一下普通函数调用的机制:程序调用某函数?函数执行,程序等待?函数将结果和控制权返回给程序?程序继续处理. Reactor释义"反应堆",是一种事件驱动机制.和普通

libevent源码深度剖析四

libevent源码深度剖析四 --libevent源代码文件组织 1 前言 详细分析源代码之前,如果能对其代码文件的基本结构有个大概的认识和分类,对于代码的分析将是大有裨益的.本节内容不多,我想并不是说它不重要! 2 源代码组织结构 Libevent的源代码虽然都在一层文件夹下面,但是其代码分类还是相当清晰的,主要可分为头文件.内部使用的头文件.辅助功能函数.日志. libevent框架.对系统I/O多路复用机制的封装.信号管理.定时事件管理.缓冲区管理.基本数据结构和基于libevent的两

libevent源码深度剖析五

libevent源码深度剖析五--libevent的核心:事件event 张亮 对事件处理流程有了高层的认识后,本节将详细介绍libevent的核心结构event,以及libevent对event的管理. 1 libevent的核心-event Libevent是基于事件驱动(event-driven)的,从名字也可以看到event是整个库的核心.event就是Reactor框架中的事件 处理程序组件:它提供了函数接口,供Reactor在事件发生时调用,以执行相应的事件处理,通常它会绑定一个有效

libevent源码深度剖析十一

libevent源码深度剖析十一 --时间管理张亮 为了支持定时器,Libevent必须和系统时间打交道,这一部分的内容也比较简单,主要涉及到时间的加减辅助函数.时间缓存.时间校正和定时器堆的时间值调整等.下面就结合源代码来分析一下. 1 初始化检测 Libevent在初始化时会检测系统时间的类型,通过调用函数detect_monotonic()完成,它通过调用clock_gettime()来检测系统是否支持monotonic时钟类型: [cpp] view plaincopy static v

libevent源码深度剖析七

libevent源码深度剖析七 --事件主循环张亮 现在我们已经初步了解了libevent的Reactor组件--event_base和事件管理框架,接下来就是libevent事件处理的中心部分 --事件主循环,根据系统提供的事件多路分发机制执行事件循环,对已注册的就绪事件,调用注册事件的回调函数来处理事件. 1 阶段性的胜利 Libevent将I/O事件.定时器和信号事件处理很好的结合到了一起,本节也会介绍libevent是如何做到这一点的.     在看完本节的内容后,读者应该会对Libev

libevent源码深度剖析八

libevent源码深度剖析八 --集成信号处理张亮 现在我们已经了解了libevent的基本框架:事件管理框架和事件主循环.上节提到了libevent中I/O事件和Signal以及Timer事件的集成,这一节将分析如何将Signal集成到事件主循环的框架中. 1 集成策略--使用socket pair 前一节已经做了足够多的介绍了,基本方法就是采用"消息机制".在libevent中这是通过socket pair完成的,下面就来详细分析一下.      Socket pair就是一个s

libevent源码深度剖析十二

libevent源码深度剖析十二 --让libevent支持多线程张亮 Libevent本身不是多线程安全的,在多核的时代,如何能充分利用CPU的能力呢,这一节来说说如何在多线程环境中使用libevent,跟源代码并没有太大的关系,纯粹是使用上的技巧. 1 错误使用示例 在多核的CPU上只使用一个线程始终是对不起CPU的处理能力啊,那好吧,那就多创建几个线程,比如下面的简单服务器场景.1 主线程创建工作线程1:2 接着主线程监听在端口上,等待新的连接:3 在线程1中执行event事件循环,等待事