定时器原理
发布日期:2021-06-23 04:43:52 浏览次数:7 分类:技术文章

本文共 1921 字,大约阅读时间需要 6 分钟。

1. 定时器介绍

程序里的定时器主要实现的功能是在未来的某个时间点执行相应的逻辑。在定时器模型中,一般有如下几个定义。 

interval:间隔时间,即定时器需要在interval时间后执行

StartTimer:添加一个定时器任务
StopTimer:结束一个定时器任务
PerTickBookkeeping: 检查定时器系统中,是否有定时器实例已经到期,相当于定义了最小时间粒度。

常见的实现方法有如下几种:

链表

排序链表
最小堆
时间轮 

接下来我们一起看下这些方法的具体实现原理。

 

2. 定时器实现方法

2.1 链表实现


链表的实现方法比较粗糙。链表用于存储所有的定时器,每个定时器都含有interval 和 elapse 两个时间参数,elapse表示当前被tickTimer了多少次。当elapse 和interval相等时,表示定时器到期。

在此方案中,添加定时器就是在链表的末尾新增一个节点,时间复杂度是 O(1)。

如果想要删除一个定时器的话,我们需要遍历链表找到对应的定时器,时间复杂度是O(n)。

此方案下,每隔elapse时间,系统调用信号进行超时检查,即PerTickBookkeeping。每次PerTickBookkeeping需要对链表所有定时器进行 elapse++,因此可以看出PerTickBookkeeping的时间复杂度是O(N)。

可以看出此方案过于粗暴,所以使用场景极少。

2.2 排序双向链表实现


排序双向链表是在链表实现上的优化。优化思路是降低时间复杂度。

首先,每次PerTickBookkeeping需要自增所有定时器的elapse变量,如果我们将interval变为绝对时间,那么我们只需要比较当前时间和interval时间是否相等,减少了对每个定时器的操作。

如果不需要对每个定时器进行操作,我们将定时器进行排序,那么每次PerTickBookkeeping都只需要判断第一个定时器,时间复杂度为O(1)。

相应的,为了维持链表顺序,每次新增定时器需要进行链表排序时间复杂度为 O(N)。

每次删除定时器时,由于会持有自己节点的引用,所以不需要查找其在链表中所在的位置,所以时间复杂度为O(1),双向链表的好处。

图1 双向链表实现示意图

 

2.3 时间轮实现


相信上一篇文章《》我们已经对时间轮有了很深刻的了解。时间轮示意图如下:

图2 时间轮

时间轮的数据结构是数组 + 链表。 

他的时间轮为数组,新增和删除一个任务,时间复杂度都是O(1)。

PerTickBookkeeping每次转动一格,时间复杂度也是O(1)。

2.4 最小堆实现


最小堆是堆的一种, (堆是一种二叉树), 指的是堆中任何一个父节点都小于子节点, 子节点顺序不作要求。

二叉排序树(BST)指的是: 左子树节点小于父节点, 右子树节点大于父节点, 对所有节点适用

 

 

图3 最小堆

树的基本操作是插入节点和删除节点。对最小堆而言,为了将一个元素X插入最小堆,我们可以在树的下一个空闲位置创建一个空穴。如果X可以放在空穴中而不被破坏堆的序,则插入完成。否则就执行上滤操作,即交换空穴和它的父节点上的元素。不断执行上述过程,直到X可以被放入空穴,则插入操作完成。

因此我们可以知道最小堆的插入时间复杂度是O(lgN)。

最小堆的删除和插入逻辑基本类似,如果不做优化,时间复杂度也是O(lgN),但是实际实现方案上,做了延迟删除操作,时间复杂度为O(1)。

延迟删除即设置定时器的执行回调函数为空,每次最小堆超时,将触发pop_heap,pop会重新调整最小堆,最终删除的定时器将调整到堆顶,但是回调函数不处理。

可以看到PerTickBookkeeping只处理堆顶定时器,时间复杂度O(1)。

最小堆可以使用数组来进行表示,数组中,当前下标n的左子节点为2N + 1,当前下标n的右子节点小标为2N + 2。

图4 最小堆的数组表示

 

3. 定时器不同实现对比

3.1 时间复杂度对比


图5 不同实现时间复杂度

从上面的介绍来看,时间轮的时间复杂度最小、性能最好。

3.2 使用场景来看

在任务量小的场景下:最小堆实现,可以根据堆顶设置超时时间,数组存储结构,节省内存消耗,使用最小堆可以得到比较好的效果。而时间轮定时器,由于需要维护一个线程用来拨动指针,且需要开辟一个bucket数组,消耗内存大,使用时间轮会较为浪费资源。

在任务量大的场景下:最小堆的插入复杂度是O(lgN), 相比时间轮O(1) 会造成性能下降。更适合使用时间轮实现。

在业界,服务治理的心跳检测等功能需要维护大量的链接心跳,因此时间轮是首选。

转载地址:https://blog.csdn.net/s2603898260/article/details/106780233 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:知乎大佬图文并茂的epoll讲解,看不懂的去砍他
下一篇:github上使用C语言实现的线程池

发表评论

最新留言

第一次来,支持一个
[***.219.124.196]2024年04月07日 03时30分57秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

攻击性手动Web渗透测试框架TIDoS-Framework 2019-04-26
计算机网络:17---混杂模式 2019-04-26
TCP/IP卷一:58---UDP之(路径MTU发现机制(PMTUD)、UDP最大数据报长度(数据报截断)、UDP/IPv4和UDP/IPv6数据报的转换) 2019-04-26
Linux(内核剖析):23---中断下半部之(下半部总体概述) 2019-04-26
Linux(内核剖析):24---中断下半部之(软中断机制(struct softirq_action、softirq_vec)) 2019-04-26
Linux(内核剖析):25---中断下半部之(tasklet机制(struct tasklet_struct)、BH机制) 2019-04-26
Linux(内核剖析):26---中断下半部之(工作队列机制(workqueue_struct、cpu_workqueue_struct)) 2019-04-26
TCP/IP卷一:63---TCP基础之(ARQ和重传、分组窗口和滑动窗口、流量控制和拥塞控制、设置重传超时) 2019-04-26
TCP/IP卷一:64---TCP基础之(传输控制协议(TCP)概述、TCP的可靠性、TCP报文格式与封装) 2019-04-26
Linux(内核剖析):27---中断下半部之(下半部机制的选择、在下半部之间加锁、禁止下半部(local_bh_disable、local_bh_enable)) 2019-04-26
TCP/IP卷一:65---TCP连接管理之(TCP连接的建立与终止、TCP半关闭、同时打开/同时关闭、初始化序列号、连接与转换器(NAT)) 2019-04-26
C++(对象模型):08---Function之(Function Member的各种调用方式(静态函数、非静态函数、虚函数)、附C++的mangling机制) 2019-04-26
MySQL数据类型——字符串类型(CHAR、BINARY、BLOB、TEXT、ENUM、SET) 2019-04-26
Effective STL第7条:容器之(如果容器内元素通过new创建,切记在容器对象析构前将指针delete掉(使用智能指针)) 2019-04-26
TCP/IP卷一:66---TCP连接管理之(TCP选项(最大段大小/选择确认/窗口缩放/时间戳/用户超时/认证选项)) 2019-04-26
TCP/IP卷一:67---TCP连接管理之(分组层路径最大传输单元发现(PLPMTUD)、黑洞问题、黑洞探测) 2019-04-26
TCP/IP卷一:68---TCP连接管理之(TCP状态转换图、TIME_WAIT状态、静默时间、FIN_WAIT_2状态、同时打开/同时关闭的状态) 2019-04-26
MySQL事务与事务隔离级别(START TRANSACRION、COMMIT、ROLLBACK、SAVEPOINT、autocommit) 2019-04-26
MySQL数据类型——数字类型(整型、浮点型、BIT型) 2019-04-26
MySQL访问权限管理(grant、revoke) 2019-04-26