zoukankan      html  css  js  c++  java
  • libco 的定时器实现: 时间轮

    一、简介

      定时器是网络框架中非常重要的组成部分,往往可以利用定时器做一些超时事件的判断或者定时清理任务等。

      定时器有许多经典高效的实现。例如,libevent 采用了小根堆实现定时器,redis 则结合自己场景直接使用了简单粗暴的双向链表

      时间轮也是一个非常经典的定时器实现,Linux 2.6 内核之前就采用了多级时间轮作为其低精度定时器的实现。而在微信的协程库 libco 中,也用了单级时间轮来处理其内部的超时事件。

      在 libco 的时间轮中,对超时事件的添加删除查询操作均可以达到 O(1) 的时间复杂度,是一个非常高效的数据结构。

    二、时间轮的表示

      libco 的时间轮的数据结构定义如下:

     1 struct stTimeout_t
     2 {
     3     /**
     4      * pItems 是一个数组,数组长度为 iItemSize。而数组中的每个元素是 stTimeoutItemLink_t 类型,
     5      * 这是一个双向链表实现。而同一个链表中的每个元素,它们的超时时间都是相同的。
     6      * libco 的时间轮是一个环形数组的实现
     7      */
     8     stTimeoutItemLink_t *pItems;
     9     int iItemSize;
    10 
    11     //ullStart 代表当前最近超时时间的时间戳,单位是毫秒
    12     unsigned long long ullStart;
    13     //llStartIdx 代表当前最近超时时间对应的 index
    14     long long llStartIdx;
    15 };

      时间轮 stTimeout_t 负责 libco 中所有超时事件的管理, 其中各属性的意义如下:

        1、pItems是一个数组,数组长度为 iItemSize。而数组中的每个元素是 stTimeoutItemLink_t 类型,这是一个双向链表实现。而同一个链表中的每个元素,它们的超时时间都是相同的。

        2、llStartIdx代表当前最近超时时间对应的 index。

        3、ullStart代表当前最近超时时间的时间戳,单位是毫秒

      总体来说,libco 的时间轮是一个环形数组的实现,如下图所示:

      在这个环形数组中,数组中每个元素代表 1ms。而 libco 将环形数组的总长度设为 60*1000,即最多可以表达 1 分钟以内的超时事件,且超时精度是毫秒

      而且,有可能会有多个超时事件在同一时刻发生,因此数组中的元素是个链表,代表同在该时刻触发的超时事件

      在 libco 初始化时,ullStart被初始化为当前时刻的时间戳 (单位为毫秒),llStartIdx初始化为 0。

    三、添加一个超时事件

    3.1 相对时间转化为时间戳

      我们看下 libco 是怎么添加一个超时事件的,第一步将相对时间转化为时间戳,这点不难理解,只有统一成标准的时间表示,才可以和其他超时事件统一的放在一起。代码如下:

    1    // 获取当前时间
    2     unsigned long long now = GetTickMS();
    3     // 超时时间
    4     arg.ullExpireTime = now + timeout;

    3.2 时间差

      计算该超时事件的触发时间距离时间轮中最近的超时时间 ullStart 的时间差值,为了下一步确定其在数组中的位置。

    1    //计算该超时事件的触发时间距离时间轮中最近的超时时间 ullStart 的时间差值
    2     unsigned long long diff = apItem->ullExpireTime - apTimeout->ullStart;

      计算得到了这个时间差值,才可以进一步计算新的超时事件在时间轮中的位置。 当然,在把超时事件放入时间轮之前,需要先判断下该超时事件是否越界了。如果比 ullStart 大于 1 分钟,

    则 libco 时间轮没有办法表示这个超时事件,将会报错。相关代码如下:

     1    /**
     2      * 如果比 ullStart 大于 1 分钟, 则 libco 时间轮没有办法表示这个超时事件,将会报错
     3      */
     4     if( diff >= (unsigned long long)apTimeout->iItemSize )//超出最大时间刻度
     5     {
     6         diff = apTimeout->iItemSize - 1;
     7         co_log_err("CO_ERR: AddTimeout line %d diff %d",
     8                     __LINE__,diff);
     9 
    10         //return __LINE__;
    11     }

    3.2 位置确定以及插入

      这里其实有两步: 计算在时间轮中的位置。其实很简单,就是一个简单的取余过程,这个也是环形数组的普遍做法。得到的( apTimeout->llStartIdx + diff ) % apTimeout->iItemSize,即是

    该超时事件所在的位置。 将该超时事件追加在该位置上的链表最后。前面讲过,有可能多个超时事件可能会在同一时刻触发。

    四、超时事件的激活

      libco 是如何判断事件是否超时以及取出所有已超时的事件呢?过程如下:

      • 如果当前的时间小于 ullStart ,说明目前没有事件超时
      • 如果大于等于 ullStart ,用当前时间减去 ullStart ,就可以得出一共过去了多少毫秒,一毫秒代表一个数组元素,从 llStartIdx 开始遍历即可

    五、总结

      时间轮是典型的空间换时间的做法,需要预先把环形数组的内存空间都分配好,这也是 libco 的超时事件存取高效的原因

      讲到这里,其实 libco 的整个时间轮算法已经全部分析完成了。

    5.1 存在的问题

      但是对于 libco 的时间轮大家可能会有一些疑问:

        1. libco 的时间轮最多只能支持 1 分钟的超时时间。虽然这个时间对于后台服务的场景已经完全足够了,但是如果我们在其他场景需要更长的超时时间呢

        2. libco 中的一个数组元素代表 1ms。如果我们需要更长的时间那岂不是内存空间也随之线性增长了

    5.2 优化

      那接下来我们就简单讲下对于时间轮的进一步优化:

        1、单级时间轮的优化。我们可以对 libco 的单级时间轮做一些简单的优化,例如给每个超时事件加一个 rotation 参数,代表该超时事件会在第几轮触发,这样就可以在一个单级时间轮中存放无限长的超时事件了。但这样代价是超时事件的判断和取出将不会是O(1)了

        2、多级时间轮。Linux 内核中就采用了多级时间轮的机制,模拟了现实生活中水表刻度。即第一级的时间轮与普通的单级时间轮相同,而第二级时间轮的每个元素的时长等于第一级时间轮的全部总时长,依次类推。Linux 内核中一共采用了五级时间轮。第一级的时间轮所有事件消耗完成后,会触发第二级时间轮的事件迁移。

    5.3 多级事件轮(分层时间轮)

      分层时间轮是这样一种思想:

      • 针对时间复杂度的问题:不做遍历计算round,凡是任务列表中的都应该是应该被执行的,直接全部取出来执行。
      • 针对空间复杂度的问题:分层,每个时间粒度对应一个时间轮,多个时间轮之间进行级联协作。

      第一点很好理解,第二点有必要举个例子来说明:

      比如我有三个任务:

        任务一每周二上午九点。

        任务二每周四上午九点。

        任务三每个月12号上午九点。

      三个任务涉及到四个时间单位:小时、天、星期、月份。

      拿任务三来说,任务三得到执行的前提是,时间刻度先得来到12号这一天,然后才需要关注其更细一级的时间单位:上午9点。

      基于这个思想,我们可以设置三个时间轮:月轮、周轮、天轮。

      月轮的时间刻度是天。

      周轮的时间刻度是天。

      天轮的时间刻度是小时。

      初始添加任务时,任务一添加到天轮上,任务二添加到周轮上,任务三添加到月轮上

      三个时间轮以各自的时间刻度不停流转。

      当周轮移动到刻度2(星期二)时,取出这个刻度下的任务,丢到天轮上,天轮接管该任务,到9点执行

      同理,当月轮移动到刻度12(12号)时,取出这个刻度下的任务,丢到天轮上,天轮接管该任务,到9点执行。

      这样就可以做到既不浪费空间,有不浪费时间。

      整体的示意图如下所示:

    5.3 时间轮的应用

      时间轮的思想应用范围非常广泛,各种操作系统的定时任务调度,Crontab,还有基于java的通信框架Netty中也有时间轮的实现,几乎所有的时间任务调度系统采用的都是时间轮的思想。
    至于采用round型的时间轮还是采用分层时间轮,看实际需要吧,时间复杂度和实现复杂度的取舍。

    六、参考文章

    https://blog.csdn.net/xinzhongtianxia/article/details/86221241

    https://zhuanlan.zhihu.com/p/97464445

    本文来自博客园,作者:Mr-xxx,转载请注明原文链接:https://www.cnblogs.com/MrLiuZF/p/15207035.html

  • 相关阅读:
    曾国藩家书人但有恒、事无不成
    pythonredis
    tableSorter使用介绍
    Python模块学习 subprocess 创建子进程
    曾国藩家书用人必先知人
    身份证号码的规则及验证原理
    KeyDown,KeyPress 和KeyUp 之我谈(更新版本)
    Python基础综合练习
    熟悉常用的Linux操作
    大数据概述
  • 原文地址:https://www.cnblogs.com/MrLiuZF/p/15207035.html
Copyright © 2011-2022 走看看