定时器时间轮算法

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)

大家都在看

  • jd-gui反编译报错// INTERNAL ERROR //

    最近在反编译class和jar包的时候,发现部分class无法反编译出来,换了最新版本的jd-gui和多个版本都不行,只能放弃了 解决方案:GitHub上找Luyten这个工具反编…

    技术杂谈 2023年5月31日
    088
  • Chrome Service Model

    John Abd-El-Malek February 2016 Objective Move Chrome codebase towards a service-oriented …

    技术杂谈 2023年5月31日
    0118
  • 目标和现状之间都是问题

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    技术杂谈 2023年5月31日
    096
  • Sentinel 热点参数限流

    https://github.com/alibaba/Sentinel/wiki/%E7%83%AD%E7%82%B9%E5%8F%82%E6%95%B0%E9%99%90%E6%…

    技术杂谈 2023年5月31日
    0111
  • 浅析渗透测试之手动和自动的优缺点

    手动渗透测试和自动渗透测试本是出于相同的目的,即帮助企业主动发现漏洞,了解现有安全措施的成效或不足。它们之间的唯一区别就是执行方式不同,手动渗透测试是由人工来完成,自动渗透测试是由…

    技术杂谈 2023年5月31日
    095
  • YDWE Keynote

    【 YDWE Keynote】 1、使用YDWE制作的地图,需要在禁用黑色阴影、迷雾。否则进入游戏将漆黑一片,什么都看不到。 2、 3、 4、 5、 6、 Original: ht…

    技术杂谈 2023年5月31日
    0110
  • WebMagic爬虫Demo

    各位看官可以关注博主个人博客,了解更多信息。作者:Surpasser链接地址:https://surpass.org.cn 前言 WebMagic介绍 Java的可伸缩Web搜寻器…

    技术杂谈 2023年7月25日
    070
  • 【Golang】golang实现urlencode urldecode函数

    golang实现urlencode函数、 urldecode函数,url加解密函数 func UrlEncode(str string) string { return url.Q…

    技术杂谈 2023年6月1日
    063
  • Java并发编程-线程池

    重点内容 线程池的使⽤ 创建线程池 提交任务 关闭线程池 线程池的原理 合理配置线程池 线程池的监控 1.线程池的创建 new ThreadPoolExecutor(corePoo…

    技术杂谈 2023年7月11日
    068
  • 等宽字体、衬线字体与非衬线字体(转)

    本文转自:https://howiezhao.github.io/2018/09/23/code-font/ 等宽字体与比例字体 等宽字体(Monospaced)是指字符宽度相同的…

    技术杂谈 2023年6月1日
    092
  • Open Physics

    1、开放物理计划。 开放物理计划,英文Open Physics。是AMD公司为自己的3A平台打造的物理模拟计算平台,以OpenCL为基础,由CPU+GPU联合计算完成。所谓&#82…

    技术杂谈 2023年5月31日
    0101
  • C# Task的用法详解 z

    1、Task的优势ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。比如:◆ ThreadPool不支持线程的取消、完成、…

    技术杂谈 2023年6月1日
    067
  • 写在进队之后

    CCF NOI 还没有发今年的题面,我只好把补题计划向后推迟。虽然我记得每道题目具体在讲什么,也不是不能补题,但我打算做些更有意义的。 从发榜到现在已经过去了 28 个小时,我整个…

    技术杂谈 2023年6月21日
    091
  • 《Generative Adversarial Networks for Hyperspectral Image Classification 》论文笔记

    GAN是一种新的模型,通常包含生成模型G和判别模型D。模型G和D以对抗性的方式进行训练,其中G试图生成尽可能真实的假输入,D试图对真实输入和假输入进行分类。在这个对抗性博弈中,双方…

    技术杂谈 2023年6月21日
    099
  • Aerospke admin

    Aerospke admin posted on2022-02-09 17:43 duanxz 阅读(22 ) 评论() 编辑 Original: https://www.cnbl…

    技术杂谈 2023年5月30日
    0111
  • 2-08. 用扑克牌计算24点(25) (ZJU_PAT 数学 枚举)

    一副扑克牌的每张牌表示一个数(J、Q、K分别表示11、12、13,两个司令都表示6)。任取4张牌。即得到4个1~13的数,请加入运算符(规定为加+ 减- 乘* 除/ 四种)使之成为…

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