在上篇中提了定时器的两种实现的思路,同时也提到了一个问题,就是当一个进程需要使用大量定时器时,需要利用时间轮、最小堆或红黑树等结构来管理定时器。为什么要这样呢?我们先回顾下上篇的观点,上篇中提到定时器比较常见的有两种,一种是cron like,另外一种是周期性执行的定时器,但是对于cron like的定时器而言,如果要性能好,一般要每个定时器一个线程,或者是要借用另外一个周期执行的定时器来做触发源,用一个数据结构,如链表,把所有cron like的定时器串起来,然后在每个周期,如1秒一次,去扫描这个链表,去判断是否有定时器能够被触发。这种做法在定时器比较少时固然问题不大,但是如果定时器多起来的话,整个过程的性能就不那么的好了,所以,我们需要有一个比较链表更好的数据结构来管理我们的定时器。对于定时器管理这个场景中,用链表的查找慢的原因主要是因为定时器在链表中是无序保存的,所以只能够遍历整个链表,那要改进的话,就必须解决一个问题,就是怎样把这些定时器变得有序。这个问题最简单的解法,就是预先计算出这些定时下次被触发的时间点,然后根据这个时间点由小到大排序就行了,但是如果还是沿用链表,每个有节点要增加或者移除,性能都不会太好,所以,我们必须引入更好的数据结构来管理这些定时器。下面说下笔者认为比较常见的思路吧。
1、红黑树
对于改进链表的查找性能,最直接也是最常见的思路,就是使用二分搜索树。但是由于定时器执行的时间点是一直在变化的,也就是说,如果这棵二分查找树的根节点不变的情况下,二分查找树会变得很不平衡,因此,使用能够自动调节高度的红黑树会比使用普通的二分查找树会来的更加合理。但是由于红黑树的实现比较长,而且大部分算法书里面都会有提到,所以这里就不去啰嗦了,大家自行参考这里吧。
2、最小堆
红黑树在解决问题的角度看,却是一个不错的选择,不管是插入还是查找还是删除都是lgN的时间复杂度,无奈要实现一棵正确的红黑树还是有一定难度的,当然,现在开源的实现还是有不少的,我们可以考虑拿来主义。不过,如果想用二分查找树,但是由不想维护像红黑树这样的复杂数据结构,能怎样办呢?可能有同学想到了,没错,用堆。我们的定时器是从时间小到大的顺序执行的,所以,我们可以使用对里面的最小堆。如果不懂最小堆是什么的同学,麻烦看看教科书,或者简单点看看这里。下面提供一个用vector实现堆的思路
#include
那既然都用到了c++,stl就没有提供吗?答案是有的,那就是priority_queue,但是priority_queue默认是一个最大堆,而我们要的是最小堆,咋办呢?一个比较土的办法是把时间都变成负数,这是可以解决问题的,至于优雅程度吧,那就只能这样了,至于更优雅的办法吧,我们先看看stl中priority_queue的定义先
template
从这里可以看到,priority_queue的排序是由Compare来决定的,所以只要我们修改Compare为我们自己的排序函数,就能实现按照我们的排序方式来排序堆里面的元素了。下面举个简单的例子,
struct
3、时间轮
在讨论时间轮之前,我们先来想一个和定时器无关的问题,那就是如果我给一个任意大的整数集合R和一个整数A,怎样判断这个A是否在集合R中。方法有很多种,暴力查找,排序等等,都能完成,就是性能上好与坏的差别了,当然了,如果在空间无限大的情况下,最好的解法肯定是做一个无限大的HASH表,这样只要经常O(1)的时间就能完成这个判断了。没错,在空间用不完的前提下,这确实是最优解,但是理想是丰满的,现实是骨感的,空间怎么可能没有限制的呢?那优化一下,我们做一个大小为N的闭地址HASH,这样在假定hash函数能够保证每个地址的元素个数接近的情况下,对比的次数只有原来直接对比的1/N,如果觉得这还不够,可以为每个地址再做一个二次HASH,这样查找的耗时就能够进一步减少。好了,回到正题,为什么会先讨论这个东西呢?大家看看,在定时器的实现中,我们知道一个时间值,然后我们要判断这个时间对应是否有定时任务需要触发,这和上面说的问题是不是等价的?如果是,那是不是也能用类似的手段来优化定时器的写入和查找速度呢?答案是肯定的,如果我们有无限内存的话,我们把未来的每一秒都分配一个槽,拿到时间值之后直接看那个槽有没有任务就可以了,但是理想归理想,现实还是得尊重的。我们没有无限的内存,所以我们只能考虑次优解,就是如上所说的构建一个大小为N的闭地址HASH了。假设我们简单得用取模作为HASH函数,那么,我们就可以得到一个类似下面的数据结构
![fa1f3db67ecd2bd48d822e65217a03ae.png](https://i-blog.csdnimg.cn/blog_migrate/1398ce1b44f7415b8b406f9c40c50243.jpeg)
这样,我们处理的集合就比原来小多了。当然,我们还可以对每个HASH的节点的链表进行排序或者二次HASH的操作,只是在一般应用层来说,一般使用最小堆就能满足了,所以这里就不再做展开了。这里有一篇关于LINUX内核的时间轮的实现介绍,有兴趣d的同学可以自己看看,同时附上两个比较好的时间轮实现,分别是云风的skynet和facebook的folly,这里就不做玩具级代码的展示了。