高性能定时器实现方式

文章目录

    • 0.简介
    • 1.整体分析
    • 2.定时通知的实现方式
      • 2.1 简单等待方式
      • 2.2 SIGALRM信号
      • 2.3 I/O多路复用方式
    • 3.定时任务的存储和管理
      • 3.1 简单升序链表的方式
    • 3.2 时间轮方式
      • 3.3 时间堆方式
    • 4.总结

0.简介

在实际开发中,经常会有定时去执行一个任务或者到某一时间去执行某一特定任务的需求(如心跳检测,状态检查等),此时就需要定时器去进行唤醒和调度,本文将从设计和实现的角度介绍多种定时器原理,并对其复杂度进行分析。

1.整体分析

要想实现一个完整的定时器,应该考虑如下两个问题:
1)如何进行定时通知?
2)定时任务如何存储和管理?
下面将围绕这两个问题去进行说明。

2.定时通知的实现方式

2.1 简单等待方式

简单等待其基本原理就是直接使用sleep方式让线程原地等待,等到时间到达内核会将其重新放入就绪队列等待调度。

2.2 SIGALRM信号

使用alarm和setitimer函数设置的闹钟一旦超时,就会触发SIGALRM信号,从而进入信号处理函数,可以通过它来处理定时任务,但如果要处理的定时任务比较多,就需要不断出发该信号,一般来说,会通过固定频率生成SIGALRM信号,这样的话如果不是间隔时间整数倍的定时任务执行时间可能略有偏差。其需要的是定义信号处理函数以及按时触发。

2.3 I/O多路复用方式

Linux下的I/O多路复用都可以指定超时参数,其不仅可以统一处理I/O事件,也可以统一处理定时事件,其有可能在超时时间之前返回(有I/O时间发生),所以利用其定时需要更新剩余时间。

while (true) {
        start = time (NULL);
        int number = epoll_wait (epollfd, events, MAX_EVENT_NUMBER, timeout);
        if (number < 0) {
            perror("epoll_wait() error\n");
            break;
        } else if (number == 0) {
            // 超时时间到,在这里处理超时事件
            timeout = TIMEOUT;
            continue;
        } else {
            for (int i = 0; i < number; ++i) {
                // 在这里处理其它的epoll事件
            }
        }

        end = time(NULL);
        timeout -= (end - start) * 1000;
        if (timeout <= 0) {  // 超时处理
            timeout = TIMEOUT;
        }
    }

3.定时任务的存储和管理

定时任务存储和管理有着多种实现方式,本节先介绍简单的链表方式,然后对高性能的时间轮和时间堆进行介绍。

3.1 简单升序链表的方式

简单升序列表思想和实现都比较简单,就是将任务使用链表管理,把剩余时间少的放到链表前面,这样就能挨个取出判断是否执行即可,插入复杂度为O(n),删除和执行都是O(1)复杂度。
高性能定时器实现方式_第1张图片

3.2 时间轮方式

简单升序链表虽然简单,但其插入复杂度比较高,下面会介绍时间轮方式,其可以很好的解决这个问题,一个简单的时间轮如下图所示:

高性能定时器实现方式_第2张图片
在这个单层的时间轮中,实线指针指向轮子上的一个槽(slot)。它以恒定的速度顺时针转动,每转动一步就指向下一个槽(slot)。每次转动称为一个滴答(tick)。一个tick时间间隔为时间轮的si(slot interval)。该时间轮共有N个槽,因此它转动一周的时间是Nsi.每个槽指向一条定时器链表,每条链表上的定时器具有相同的特征:它们的定时时间相差Nsi的整数倍。时间轮正是利用这个关系将定时器散列到不同的链表中。假如现在指针指向槽cs,我们要添加一个定时时间为ti的定时器,则该定时器将被插入槽ts(timer slot)对应的链表中:ts=(cs+(ti/si))%N。

基于排序链表的定时器使用唯一的一条链表来管理所有的定时器,所以插入操作的效率随着定时器的数目增多而降低。而时间轮使用了哈希表处理冲突的思想,将定时器散列到不同的链表上。这样每条链表上的定时器数目都将明显少于原来的排序链表上的定时器数目,插入操作的效率基本不受定时器数目的影响。
很显然,对于时间轮而言,要提高精度,就要使si的值足够小; 要提高执行效率,则要求N值足够大,使定时器尽可能的分布在不同的槽。

时间轮也可以多层,多个轮子,相邻的轮子,精度高的转一圈,精度低的移动一个槽。像Kafka,netty中都有用到。

该方式将插入时间复杂度降到了O(1)。

3.3 时间堆方式

时间堆方式是利用最小堆来进行管理,插入时间复杂度为O(logN),同时也能达到取最接近任务的目的,其结构如图所示。

高性能定时器实现方式_第3张图片

4.总结

以上就对定时器面对两个问题的常见解决方案进行了描述,实际中可以根据需要来选择不同的实现方式。

你可能感兴趣的:(服务器,网络,服务器)