定时器是很多程序都需要使用到的功能。
现代操作系统有一个很重要的功能——进程切换,执行一个进程一半的时候跑去执行另一个进程。这是怎么做到的呢?
就是由可编程硬件定时器发出信号(操作系统启动的时候设置),触发了操作系统注册的处理函数,在这个处理函数里把进程切换掉。
硬件定时器是很珍贵的,我们基本上都是使用软件定时器。软件定时器说白了就是一个线程一直跑,看到注册的时间到了,就调用相应的回调函数。
用c++描述软件定时器设计如下:
class TimeoutHandler // 使用接口的方式而不是boost::function,一方面是出于效率考虑,另一方面是这样便于理解
{
public:
virtual ~TimeoutHandler(){}
virtual void onTimeout() = 0;
};
class TimerMonitor
{
int setTimer(int inteval, boost::shared_ptr<TimeoutHandler> handler); // 为何用智能指针?这种回调类的都很怕回调的时候对象被析构,智能指针是最简单解决办法
void stopTimer(int timer_id);
// 删除,根据setTimer返回的timer_id进行删除,函数返回后保证不会再调用handler->onTimeout()
void run(int check_inteval);
// 以设置好的检测周期开始监控线程
void stop();
// 停止监控线程
};
那具体如何实现TimerMonitor呢?
最简单低效的做法就是维持一个数组/非排序列表:定时器线程一直跑,检测超时时轮询所有设置的时间,如果超过了就执行相应回调函数。这种算法每次检测要遍历所有设置的时间,无疑是低效的0(n)。插入O(1)删除O(n)。
稍微高级的做法就是使用排序链表,最前面就是最近要到期的,检测超时时不需要轮询,只需要看看头几个结点即可(可视为O(1))。插入和删除O(n)。
之前我知道的比较好的方法是最小堆,检测超时时也是O(1),插入0(lgn),删除O(n)——注意如果不引入其他数据结构的情况下只能通过遍历找到timer_id对应结点。
今天刚知道的:Timing-Wheel时间轮算法,参考http://www.ibm.com/developerworks/cn/linux/l-cn-timers/。我感觉这个算法是很聪明的算法,水表的例子也非常好。我一开始看时间轮算法以为是类似TRIE的算法,后面又以为是多级Hash算法,但其实都似是而非。
Timing Wheels的本质有点像水表:高维度一单位等于低维度的一圈。
举个例子来说明吧,假设总共有两个维度,每个维度三个槽。
第一维:
slot 1-1 :链表,放置在0间隔后过期的handler
slot 1-2 :链表,放置在1间隔后过期的handler
slot 1-3 :链表,放置在2间隔后过期的handler
第二维:
slot 2-1 :链表,放置3-4-5间隔后过期的handler
slot 2-2 :链表,放置6-7-8间隔后过期的handler
slot 2-3 :链表,放置9-10-11间隔后过期的handler
注意slot1-1,slot2-1 并不是固定的,而是由这一维度的当前指向位置决定的,可以把每一个维度当做一个循环队列。步进的时候就是最低维+1,但这个+1可能会导致进位甚至连环进位。
假设有一个2*2*2的时间轮,步进时顺序是这样 0-0-0(初始) -> 1-0-0 -> 0-1-0(进位) -> 1-1-0 -> 0-0-1(连环进位))
发生进位的时候要把高纬度那个槽里的handler抓来处理,如果时间到了就触发,还没到就塞进低维度的槽中。还举3*3的例子,当触发slot2-1的时候,所有3的定时器触发,4的塞到slot1-2,5的就塞到slot1-3。
算法思想大抵是如此吧。等我哪天有空了再写代码嘿嘿。
过了n天:终于动手实现了,http://blog.sina.com.cn/s/blog_48d4cf2d0100ql2b.html。