定时器时间轮算法

Linux定时器分为低精度定时器和高精度定时器两种类型,内核对其均有实现。本文讨论的是我们在应用程序开发中比较常见的低精度定时器。作为常用的基础组件,定时器常用的几种实现方法包括:基于排序链表实现、基于小根堆实现、基于红黑树实现、基于时间轮实现。本文讲解的是时间复杂度最优,也是linux内核采用的基于时间轮的实现方式。

  1. 有序队列

  2. 添加/删除任务: 遍历每一个节点, 找到相应的位置插入, 因此时间复杂度为O(n)

  3. 处理到期任务: 取出最小定时任务为首节点, 因此时间复杂度为O(1)

  4. 红黑树
    有序队列的性能瓶颈在于插入任务和删除任务(查找排序), 而树形结构能对其进行优化。

  5. 添加/删除/查找任务: 红黑树能将排序的的时间复杂度降到O(log2N)

  6. 处理到期任务: 红黑树查找到最后过期的任务节点为最左侧节点, 因此时间复杂度为O(log2N)

  7. 最小堆

  8. 添加/查找任务: 时间复杂度为O(log2N)

  9. 删除任务: 时间复杂度为O(n), 可通过辅助数据结构(map)来加快删除操作
  10. 处理到期任务: 最小节点为根节点, 时间复杂度为O(1)

  11. 跳表

  12. 添加/删除/查找任务: 时间复杂度为O(log2N)

  13. 处理到期任务: 最小节点为最左侧节点, 时间复杂度为O(1), 但空间复杂度比较高, 为O(1.5n)

之所以没法做到O(1)的复杂度,究其原因是所有定时器节点挂在一条链表(或一棵树)上。 时间轮算法的核心思路是将定时器散列到多条链上(定时器就是指定时任务,链上的节点),是典型的空间换时间的策略。下文从单个时间轮出发讲解,逐步扩展至 linux实现定时器所采用的多级时间轮算法

简单的单时间轮

单时间轮只有一个由bucket串起来的轮子,下图所示的时间轮有8个bucket,每个bucket下链接着未来对应时刻到期的节点。假设图中相邻bucket到期时间的间隔为slot=1s,从当前时刻0s开始计时,1s时到期的定时器节点挂在bucket[1]下,2s时到期的定时器节点挂在bucket[2]下……当tick检查到时间过去了1s时,bucket[1]下所有节点执行超时动作,当时间到了2s时,bucket[2]下所有节点执行超时动作…….

定时器时间轮算法
由于bucket是一个数组,能直接根据下标定位到具体定时器节点链,因此添加删除节点、定时器到期执行的时间复杂度均为O(1)。(有Hash内味了)

但使用这个定时器所受的限制也显而易见:待添加的timer到期时间必须在8s以内。这显然不能满足实际需求。当然要扩展也很容易,直接增加bucket的个数就可以了。在 Linux 系统中,我们可以设置slot为1个jiffy(1/HZ)的定时器,假设最大的到期时间范围要达到 2^32个 jiffies,如果采用上面这样的单时间轮,我们就需要2^32个 bucket,这会带来巨大的内存消耗,显然是需要优化改进的。

改进的单时间轮

改进的单时间轮其实是一个对时间和空间折中的思路,即不会像单时间轮那样有O(1)的时间复杂度,但也不会像单时间轮那样对bucket个数有巨大的需求。其原理也很简单,就是每个bucket不单可以挂接到期时间expire=slot的定时器,还可挂接expire%N=slot的定时器(N为bucket个数)。这也正好顺应时间轮的轮回作用。如图2所示,定时器中expire表示到期时间, rotation表示节点在时间轮转了几圈后才到期。当当前时间指针指向某个bucket时,不能像简单时间轮那样直接对bucket下的所有节点执行超时动作,而是需要对链表中节点遍历一遍,判断轮子转动的次数是否等于节点中的rotation值,当两者相等时,方可执行超时操作。

定时器时间轮算法

我们的uthread项目就是这种改进型的单时间轮,效果也很好,不过我们使用的单链表,但在一些开源实现里使用的双向链表,是因为有其他场景(reset,惰性删除等)

多时间轮

上面所述的时间轮都是单枪匹马战斗的,因此很难在时间和空间上都达到理想效果。Linux所实现的多时间轮算法,借鉴了日常生活中水表的度量方法,通过低刻度走得快的轮子带动高一级刻度轮子走动的方法,达到了仅使用较少刻度即可表示很大范围度量值的效果。

定时器时间轮算法

Linux定时器时间轮分为5个级别的轮子(tv1 ~ tv5),如图3所示。每个级别的轮子的刻度值(slot)不同,规律是次级轮子的slot等于上级轮子的slot之和。Linux定时器slot单位为1jiffy,tv1轮子分256个刻度,每个刻度大小为1jiffy。tv2轮子分64个刻度,每个刻度大小为256个jiffy,即tv1整个轮子所能表达的范围。相邻轮子也只有满足这个规律,才能达到”低刻度轮子转一圈,高刻度轮子走一格”的效果。tv3,tv4,tv5也都是分为64个刻度,因此容易算出,最高一级轮子tv5所能表达的slot范围达到了25664646464 = 2^32 jiffies。

Linux时间轮定时器算法的关键在于添加定时器操作和时间轮进位迁移链表操作。先来说添加定时器。添加定时器的关键又在于知道每个时间轮每一个刻度所能表示的到期时间的范围。图4列出了每一级时间轮能度量的jiffies的大小。假设有一个定时器在1000个jiffies后到期,根据图4容易看出其应该挂在tv2轮上。tv2轮每个刻度表示的大小为256个jiffies,则其应该挂在(1000/256)=3即第三个bucket上。因此可以O(1)的添加定时器。

Linux在定时器到期检查上的操作也实现得很巧妙。假设curr_time=0x12345678,那么下一个检查的时刻为0x12345679。如果tv1.bucket[0x79]上链表非空,则下一个检查时刻tv1.bucket[0x79]上的定时器节点超时。如果curr_time到了0x12345700,低8位为空,说明有进位产生,这时移出8~13位对应的定时器链表(即正好对应着tv2轮),重新加入定时器系统(放到tv1轮),这就完成了一次进位迁移操作。同样地,当curr_time的第8-13位为0时,这表明tv2轮对tv3轮有进位发生,将curr_time第14-19位的值作为下标,移出tv3中对应的定时器链表,然后将它们重新加入到定时器系统中来(放到tv2或tv1)。tv4,tv5依次类推。遍历 tv1中该 tick 的双向循环链表,执行这些到期定时器的回调函数。之所以能够根据curr_time来检查超时链,是因为tv1~tv5轮的度量范围正好依次覆盖了整型的32位:tv1(1-8位),tv2(9-14位),tv3(15-20位),tv4(21-26位),tv5(27-32位);而curr_time计数的递增中,低位向高位的进位正是低级时间轮转圈带动高级时间轮走动的过程。

这里有一个实现细节,我们用一个int就能表示这5级时间轮:

| 6bit | 6bit | 6bit | 6bit |  8bit  |
 111111 111111 111111 111111 11111111

这样的话有一个优点,每次只需要将这个int整数+1,每一级时间轮都会自动进位。

对比

最后比较一下多级时间轮和单个简单时间轮的时间复杂度及空间复杂度:linux使用了总计256+64+64+64+64=512个bucket,即可实现[0,2^32) jiffies的超时范围。相比简单的单时间轮,时间上仅仅多了1/256次(为约等于值,忽略了tv2以上产生的进位操作)的链表迁移操作耗时。可以认为其添加、删除定时器节点及到期check的操作时间复杂度均为O(1)。

参考链接:
1. CSDN-时间轮定时器实现
2. 浪的不轻-多级时间轮定时器
3. changan’s blog-linux定时器时间轮算法

Original: https://www.cnblogs.com/lfri/p/15917019.html
Author: Rogn
Title: 定时器时间轮算法

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/549218/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • flutter创建项目

    一、终端方式 1、创建flutter项目: 出现: 2、先直接打开Xcode自带iOS模拟器,否则会运行在macOS (macos)或Chrome (chrome)上。 3、运行项…

    技术杂谈 2023年6月1日
    081
  • Vue系列—源码调试(二)

    我们要对Vue源码进行分析,首先我们需要能够对vue源码进行调式(这里的源码调式是ES6版本的,不是打包后的代码),因此首先我们要去官方github上克隆一份vue项目下来,如下具…

    技术杂谈 2023年6月1日
    077
  • Java中如何创建不可变(immutable)类

    不可变类是指类的实例一经创建完成,这个实例的内容就不会改变。 Java中的String和八个基本类型的包装类(Integer, Short, Byte, Long, Double,…

    技术杂谈 2023年6月21日
    091
  • 免费SVN空间

    推荐免费的svn空间(SVN代码托管) 最近研究了国内和国外的免费svn空间,SVN代码托管,SVN在线,代码托管中心,有所心得。 http://www.svn999.com/ […

    技术杂谈 2023年5月31日
    092
  • 深度学习中数据集很小是一种什么样的体验

    前言 今天提一个比较轻松的话题,简单探讨数据集大小对深度学习训练的影响。不知道大家有没有看过这篇文章:Don’t use deep learning your data…

    技术杂谈 2023年7月11日
    058
  • 验证token

    去测试登陆时,请求头一定要带token(authorization)信息,不然401错误。 Original: https://www.cnblogs.com/145Mirro-r…

    技术杂谈 2023年6月21日
    0101
  • Java的三大特性

    Java的三大特性 封装: 隐藏内部功能的具体实现,只保留和外部交流数据的接口,将变化隔离,便于使用,提高复用性和安全性。例:汽车与发动机,不必知道发动机的实现原理,只需使用汽车给…

    技术杂谈 2023年7月24日
    087
  • 当pytest遇上poium会擦出什么火花

    当pytest遇上poium会擦出什么火花 首先,创建一个 test_sample/test_demo.py 文件,写入下面三行代码。 def test_bing(page): p…

    技术杂谈 2023年5月31日
    082
  • Kubernetes Namespaces

    Kubernetes Namespaces _ Kubernetes(K8S)中文文档_Kubernetes中文社区http://docs.kubernetes.org.cn/24…

    技术杂谈 2023年6月1日
    0106
  • 数据库时间格式处理

    使用 DateUtil转换,这个还是比较常用的一种,下面贴代码(可以直接复制使用): /** * 日期工具类,注意导包import和package * StringUtils,Da…

    技术杂谈 2023年7月25日
    071
  • 双十一云起实验室福利大放送,一年仅一次!

    双十一福利大放送! 🎁双十一云起实验室福利大放送,一年仅一次! 📱邀请好友体验场景即可赢取iPhone 14,还有任天堂switch,AirPods,1000元、500元等面额的天…

    技术杂谈 2023年7月11日
    079
  • 一切在这里

    我们播下种子,对天空祈求,滋润植物,饮下雨水,再还给土壤,诅咒寒冷,朝着太阳跳舞,随着微风起舞,由于经历过的东西而流泪,在月亮的赔本下做梦。 我们张开我们的双手迎接另一个可能充满希…

    技术杂谈 2023年5月31日
    071
  • SQL 外键知识

    当我们用主键唯一标识记录时,我们就可以在​ ​students​​表中确定任意一个学生的记录: name other columns&#…

    技术杂谈 2023年7月11日
    058
  • Ubuntu 20.04搭建LAMP环境-20220711

    一.安装Apache 1.运行以下命令,更新Ubuntu系统内的软件包 sudo apt update 2.运行以下命令,安装Apache sudo apt-get -y inst…

    技术杂谈 2023年7月11日
    074
  • Windows下安装MySQL

    Windows下安装MySQL 安装包下载 官网下载地址https://dev.mysql.com/downloads/mysql/ 点击下载之后会跳转界面 可以不选择注册或登录账…

    技术杂谈 2023年7月24日
    065
  • 【大话QT之十二】基于CTK Plugin Framework的插件版本号动态升级

    应用需求: 某些场景下我们可能面临这种问题,在执行着的应用程序不能终止的情况下,升级某个功能(或添,或减。或改动)。在不採用CTK Plugin Framework插件系统架构的情…

    技术杂谈 2023年5月30日
    0108
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球