epoll定时器实现系列文章:高性能定时器实现的三种方式---升序链表,时间轮,最小堆(★firecat推荐★)

简介: epoll定时器实现系列文章:高性能定时器实现的三种方式---升序链表,时间轮,最小堆(★firecat推荐★)

就是说,有N多个定时器,如何来实现高效到点执行定时函数的问题。


在开发Linux网络程序时,通常需要维护多个定时器,如维护客户端心跳时间、检查多个数据包的超时重传等。


定时器的结构有多钟比如链表式,最小堆,时间轮等。在不同应用场景下使用哪种需要考虑效率和复杂度?



时间轮定时器

1.时间轮定时器有什么好处,或者说这种结构的定时器能决解什么问题?

在升序链表定时器里,可以看到在添加一个定时器的时候,复杂度是O(n)

因为要保持有序性,所以的遍历链表插入到合适的位置。假设系统有大量的定时器(10W个)

使用升序链表型的就会有性能问题。这时时间轮定时器就会比较适合。


2.常用定时器实现算法复杂度

实现方式 StartTimerStopTimerPerTickBookkeeping

基于链表 O(1)     O(n)    O(n)

基于排序链表 O(n)     O(1)    O(1)

基于最小堆 O(lgn)     O(1)    O(1)



基于时间轮 O(1)     O(1)    O(1)



3.定时器管理:nginx的红黑树和libevent的堆

libevent 发生超时后,while循环一次从堆顶del timer——直到最新调整的最小堆顶不是超时事件为止,(实际是del event),但是会稍后把这个timeout的 event放到active 任务list里,等待处理,event标记为timeout,等处理actvie队列时再由应用层callback函数决定怎么处理标记为timeout的事件。


nginx处理超时时,直接删除红黑树中( event结构体里的 )rb node成员,同时调用应用层早已通过add timer注册好的超时handler函数。之所以没有用堆,因为每次直接从内部删除节点,而不是堆顶部。


关键点,采用堆,删除时间是O(1),但是要调整堆,logn。插入时间基本是lgn。

采用红黑树,删除节点是3次旋转,但是,找到最小节点要logn。插入时间基本是lgn。

总体看,都差不多。


堆排序和红黑树性能比较 :http://blog.sina.com.cn/s/blog_56e6a0750101b0fo.html


不谈内存,从算法上来讲

红黑树插入是最坏情况要比较2logN次(最高的高度)外加不超过两次旋转,最小堆最坏情况是logN次;

红黑树删除不需要比较只需要不超过3旋转,查找最小值需要遍历logN,如果删除最小值树调整一般很小;

最小堆查询顶节点是O(1),而删除顶节点在任何情况下都是个最坏的情况,需要比较2logN次;

红黑树的最坏情况在旋转中不断调整变化,插入性能比最小堆差,但删除最小性能却比最小堆好上几倍;

如果插入+查找最小+删除最小总体来确定红黑树和最小堆,红黑树占优,性能波动相对较小,红黑树不过多的依赖比较,相对最小堆由比较带来的的性能影响较小(如果比较是调用自定义函数、比较的是字符串等,那么性能就牺牲很大);

最小堆最大的优势在于取最小值性能是O(1),如果插入的数据基本有序,那么最小堆能获得比较好的性能, 在频繁不断地取最小值的是否满足条件的应用中有更加好的性能(如超时队列),红黑树相应就要尽量避免不必要的查询次数才能在超时队列这种应用中获得更好的性能;

从实际出发:

由于最小堆一般是采用堆的方式实现,元素访问速度远高于采用链表方式的红黑树,插入性能快红黑树好几倍,但最小堆的删除性能并不快于红黑树;

最小堆的最大缺点就在于必须是连续的内存。




4. 最近学习libevent,发现libevent1.2版本对Timer事件用的是红黑树,但是libevent2.0版本就放弃了红黑树的使用,使用小根堆。于是搜集了部分资料,


以下内容来自:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=22906954&id=4376535


libevent的最小堆源码路径是:


https://github.com/libevent/libevent/blob/release-2.1.8-stable/minheap-internal.h


首先什么是小根堆:


(1)它是一颗完全二叉树

(2)任意一个节点均小于或等于其左右子节点的关键码(大根堆相反就是了)

因此可以得知,当前树形结构的根节点就是当前整个树形结构最小的节点。。。


至于说这种堆结构有什么作用:

(1)以前本科的时候上数据结构课的时候就有讲过堆排序,好像还不错,O(nlogn)的复杂度

(2)可以用来构造优先权队列。。。。

(3)在libevent库中,定时是采用小根堆来维护(nginx是红黑树)

好了,下面来看看小根堆的插入和删除吧,其实感觉 非常简单诶,比红黑树简单,只需要向上向下浮动就好了,没有红黑树那种左旋右旋,还得染色什么的。。

首先来看节点的插入:

(1)将要插入的节点按照完全二叉树的形式插入到当前树形结构的最末尾

(2)对这个刚刚插入的节点进行向上浮动,浮动的原理是:比较当前的节点和其父节点,如果比父节点小,那么与父节点交换,然后递归的进行,直到浮动不动了或者到了根节点

接下来来看节点的删除:

(1)小根堆的删除是删除当前的根节点,也就是返回当前根节点的值,然后用当前树形结构的最后一个节点来代替根节点

(2)从当前属性结构的最后一个非叶节点开始,向下浮动,浮动的原理是:

如果有比自己小的子节点,那么与这个子节点交换,然后递归的对刚刚交换下去的子节点进行向下浮动,(如果当前两个子节点都比自己小,那么就与最小的那个交换)



相关文章
|
1月前
|
C++
从0开始回顾数据结构---链表与堆
#include <iostream> #include <algorithm> #include <string.h> using namespace std; const int N = 100010; int h[N], ph[N], hp[N], cnt; void heap_swap(int a, int b) { swap(ph[hp[a]],ph[hp[b]]); swap(hp[a], hp[b]); swap(h[a], h[b]); } void down(int u) { int t = u; if (u * 2 <= cnt &&
|
3月前
|
Go
golang力扣leetcode 23.合并K个升序链表
golang力扣leetcode 23.合并K个升序链表
22 0
|
4月前
【每日一题Day293】LC23合并K个升序链表 | K指针 堆排序 归并排序
【每日一题Day293】LC23合并K个升序链表 | K指针 堆排序 归并排序
30 0
|
3月前
|
Linux
【web server】基于升序链表的定时器
【web server】基于升序链表的定时器
|
3月前
|
Java Go C++
Golang每日一练(leetDay0114) 矩阵中的最长递增路径、按要求补齐数组
Golang每日一练(leetDay0114) 矩阵中的最长递增路径、按要求补齐数组
35 0
Golang每日一练(leetDay0114) 矩阵中的最长递增路径、按要求补齐数组
|
3月前
|
Java Go C++
Golang每日一练(leetDay0096) 添加运算符、移动零
Golang每日一练(leetDay0096) 添加运算符、移动零
45 0
Golang每日一练(leetDay0096) 添加运算符、移动零
|
3月前
|
Go C++ Java
C/C++每日一练(20230408) 删除无效括号、合并K个升序链表、四数之和
C/C++每日一练(20230408) 删除无效括号、合并K个升序链表、四数之和
31 0
C/C++每日一练(20230408) 删除无效括号、合并K个升序链表、四数之和
|
3月前
|
Python Java Go
Java每日一练(20230401) 合并K个升序链表、最长有效括号、分割回文串
Java每日一练(20230401) 合并K个升序链表、最长有效括号、分割回文串
21 0
Java每日一练(20230401) 合并K个升序链表、最长有效括号、分割回文串
|
3月前
|
存储 人工智能 搜索推荐
浅谈归并排序:合并 K 个升序链表的归并解法
在面试中遇到了这道题:如何实现多个升序链表的合并。这是 LeetCode 上的一道原题,题目具体如下:
43 0
浅谈归并排序:合并 K 个升序链表的归并解法
【 腾讯精选练习 50 题】20—合并K个升序链表【困难】
【 腾讯精选练习 50 题】20—合并K个升序链表【困难】
【 腾讯精选练习 50 题】20—合并K个升序链表【困难】