一、背景
网络编程中除了处理IO事件之外,定时事件也同样不可或缺,如定期检测一个客户连接的活动状态、游戏中的技能冷却倒计时以及其他需要使用超时机制的功能。我们的服务器程序中往往需要处理众多的定时事件,因此有效的组织定时事件,使之能在预期时间内被触发且不影响服务器主要逻辑,对我们的服务器性能影响特别大。
一般的做法是将每个定时事件封装成定时器,并使用某种容器类数据结构将所有的定时器保存好,实现对定时事件的统一管理。常用方法有排序链表、红黑树、时间堆和时间轮,本篇文章将对时间堆方案进行详细介绍。
二、小根堆详解
传统的定时方案是以固定频率调用起搏函数tick,进而执行定时器上的回调函数。而时间堆的做法则是将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔,当超时时间到达时,处理超时事件,然后再次从剩余定时器中找出超时时间最小的一个,依次反复即可。
举个例子:
当前系统时间:8:00
1号定时器超时时间:8:05
2号定时器超时时间:8:08
…
设置心搏间隔:8:05-8:00=5
5分钟到达后处理1号定时器事件,再根据2号超时时间设定心搏间隔
以上是时间堆的基本设计思路,下面我们将时间堆的核心构件—最小堆进行介绍。
2.1 数据结构
小根堆:父节点的值小于或等于子节点的值,如下图:
堆的存储
一般都用数组来表示堆,i结点的父结点下标就为(i–1)/2,它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2,如下图:
由于堆存储在下标从0开始计数的数组中,因此,在堆中给定下标为i的结点时:
(1)如果i=0,结点i是根结点,无父结点;否则结点i的父结点为结点(i-1)/2;
(2)如果2i+1>n-1,则结点i无左子女;否则结点i的左子女为结点2i+1;
(3)如果2i+2>n-1,则结点i无右子女;否则结点i的右子女为结点2i+2。
2.2 相关操作
堆的插入
插入一个元素:新元素被加入到堆的末尾,然后更新树以恢复堆的次序。
每次插入都是将新数据放在数组最后。可以发现从这个新数据的父结点到根结点必然为一个有序的数列,现在的任务是将这个新数据插入到这个有序数据中——这就类似于直接插入排序中将一