时间轮算法(Timing-Wheel)很早出现在linux kernel 2.6中。因效率非常高,很多应用框架都实现了这个算法。还有些定时器使用最小堆实现,但总体来说,时间轮算法在插入性能上更高。

前面分析libco的时候,也讲到其实现了一个时间轮定时器,不过这个定时器只有一个轮,且长度是60000,仅仅实现了60秒的定时器范围,这个不免有些残缺。

这一篇想介绍一个完整的定时器实现,使用5个轮子,一共512个结点,如果精度是1毫秒,可以定时长达2^32-1毫秒这么长,粗略算起来接近50天。

时间轮分成多个层级,每一层是一个圈,和时钟类似,和水表更像,如下面图:

当个位的指针转完一圈到达0这个刻度之后,十位的指针转1格;当十位的转完一圈,百位的转1格,以此类推。这样就能表示很长的度数。

时间轮能表达的时间长度,和圈的数量以及每一圈的长度成正比。假设有5圈,每个圈60个刻度,每个刻度表示1毫秒,那么这个时间轮可以表示这么长:

60 x 60 x 60 x 60 x 60 = 777,600,000‬(ms) ~ 216小时

现在用程序表示这个指针可能这样的:

int ptr[5];

假如有一个update函数驱动时间轮运转起来,每调一次跳一格,那这个算法可能是这样的:

  • ptr[0]加1,如果ptr[0]等于60,则使ptr[0]重置为0,可以写为:ptr[0] = (ptr[0] + 1) % 60
  • 如果ptr[0]等于0的时候,说明转了一圈,此时ptr[1]要加1:ptr[1] = (ptr[1] + 1) % 60
  • 就这样一直转到第5圈,然后又重新开始。

这样处理有点麻烦,而且还需要一个数组来表示。其实可以用一个uint32变量来划分,比如:

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

也分5个圈,第1个占8位即256个槽位,后面4个分别占6位即64个槽位。这个变量只需不断自增就行,比如前面8位满了会变成0,后面自动进1。这样就可以定义一些宏:

// 第1个轮占的位数
#define TVR_BITS 8
// 第1个轮的长度
#define TVR_SIZE (1 << TVR_BITS)
// 第n个轮占的位数
#define TVN_BITS 6
// 第n个轮的长度
#define TVN_SIZE (1 << TVN_BITS)
// 掩码:取模或整除用
#define TVR_MASK (TVR_SIZE - 1)
#define TVN_MASK (TVN_SIZE - 1)

想取某一个圈的当前指针位置是:

// 第1个圈的当前指针位置
#define FIRST_INDEX(v) ((v) & TVR_MASK)
// 后面第n个圈的当前指针位置
#define NTH_INDEX(v, n) (((v) >> (TVR_BITS + (n) * TVN_BITS)) & TVN_MASK)

核心的数组结构是这样的:

// 第1个轮
typedef struct tvroot {clinknode_t vec[TVR_SIZE];
} tvroot_t;
// 后面几个轮
typedef struct tvnum {clinknode_t vec[TVN_SIZE];
} tvnum_t;// 时间轮定时器
typedef struct timerwheel {tvroot_t tvroot;               // 第1个轮tvnum_t tv[4];                 // 后面4个轮uint64_t lasttime;             // 上一次的时间毫秒uint32_t currtick;             // 当前的tickuint16_t interval;             // 每个时间点的毫秒间隔uint16_t remainder;            // 剩余的毫秒
} timerwheel_t;

这里要重点讲一下clinknode_t,这是每一个圈的槽位的数据,它其实是一个双向循环链表:

新结点可以往head的前面加,也可以往head的后面加,相当于加到链表头和链表尾。初始情况下head的前后指针指向自己。链表中的结点就是定时器结点。

双向链表的代码如下:

#pragma once
/*** 循环双向链表*/// 链表结点
typedef struct clinknode {struct clinknode *next;struct clinknode *prev;
} clinknode_t;// 初始化链表头:前后都指向自己
static inline void clinklist_init(clinknode_t *head) {head->prev = head;head->next = head;
}// 插入结点到链表的前面,因为是循环链表,其实是在head的后面
static inline void clinklist_add_front(clinknode_t *head, clinknode_t *node) {node->prev = head;node->next = head->next;head->next->prev = node;head->next = node;
}// 插入结点到链表的后面,因为是循环链表,所以其实是在head的前面
static inline void clinklist_add_back(clinknode_t *head, clinknode_t *node) {node->prev = head->prev;node->next = head;node->prev->next = node;head->prev = node;
}// 判断链表是否为空:循环链表为空是头的下一个和上一个都指向自己
static inline int clinklist_is_empty(clinknode_t *head) {return head == head->next;
}// 从链表中移除自己,同时会重设结点
static inline void clinklist_remote(clinknode_t *node) {node->next->prev = node->prev;node->prev->next = node->next;clinklist_init(node);
}// 将链表1的结点取出来,放到链表2
static inline void clinklist_splice(clinknode_t *head1, clinknode_t *head2) {if (!clinklist_is_empty(head1)) {clinknode_t *first = head1->next;       // 第1个结点clinknode_t *last = head1->prev;        // 最后1个结点clinknode_t *at = head2->next;          // 插在第2个链表的这个结点前面first->prev = head2;head2->next = first;last->next = at;at->prev = last;clinklist_init(head1);}
}

整个定时器的代码如下:

timerwheel.h

#pragma once
/*** 定时器模块*/
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include "clinklist.h"// 时间轮定时器// 第1个轮占的位数
#define TVR_BITS 8
// 第1个轮的长度
#define TVR_SIZE (1 << TVR_BITS)
// 第n个轮占的位数
#define TVN_BITS 6
// 第n个轮的长度
#define TVN_SIZE (1 << TVN_BITS)
// 掩码:取模或整除用
#define TVR_MASK (TVR_SIZE - 1)
#define TVN_MASK (TVN_SIZE - 1)// 定时器回调函数
typedef void (*timer_cb_t)(void*);// 定时器结点
typedef struct timernode {struct linknode *next;        // 下一个结点struct linknode *prev;        // 上一个结点void *userdata;               // 用户数据timer_cb_t callback;          // 回调函数uint32_t expire;              // 到期时间
} timernode_t;// 第1个轮
typedef struct tvroot {clinknode_t vec[TVR_SIZE];
} tvroot_t;// 后面几个轮
typedef struct tvnum {clinknode_t vec[TVN_SIZE];
} tvnum_t;// 时间轮定时器
typedef struct timerwheel {tvroot_t tvroot;               // 第1个轮tvnum_t tv[4];                 // 后面4个轮uint64_t lasttime;             // 上一次的时间毫秒uint32_t currtick;             // 当前的tickuint16_t interval;             // 每个时间点的毫秒间隔uint16_t remainder;            // 剩余的毫秒
} timerwheel_t;// 初始化时间轮,interval为每帧的间隔,currtime为当前时间
void timerwheel_init(timerwheel_t *tw, uint16_t interval, uint64_t currtime);
// 初始化时间结点:cb为回调,ud为用户数据
void timerwheel_node_init(timernode_t *node, timer_cb_t cb, void *ud);
// 增加时间结点,ticks为触发间隔(注意是以interval为单位)
void timerwheel_add(timerwheel_t *tw, timernode_t *node, uint32_t ticks);
// 删除结点
int timerwheel_del(timerwheel_t *tw, timernode_t *node);
// 更新时间轮
void timerwheel_update(timerwheel_t *tw, uint64_t currtime);

timerwheel.c

#include "timerwheel.h"#define FIRST_INDEX(v) ((v) & TVR_MASK)
#define NTH_INDEX(v, n) (((v) >> (TVR_BITS + (n) * TVN_BITS)) & TVN_MASK)void timerwheel_init(timerwheel_t *tw, uint16_t interval, uint64_t currtime) {memset(tw, 0, sizeof(*tw));tw->interval = interval;tw->lasttime = currtime;int i, j;for (i = 0; i < TVR_SIZE; ++i) {clinklist_init(tw->tvroot.vec + i);}for (i = 0; i < 4; ++i) {for (j = 0; j < TVN_SIZE; ++j)clinklist_init(tw->tv[i].vec + j);}
}void timerwheel_node_init(timernode_t *node, timer_cb_t cb, void *ud) {node->next = 0;node->prev = 0;node->userdata = ud;node->callback = cb;node->expire = 0;
}static void _timerwheel_add(timerwheel_t *tw, timernode_t *node) {uint32_t expire = node->expire;uint32_t idx = expire - tw->currtick;clinknode_t *head;if (idx < TVR_SIZE) {head = tw->tvroot.vec + FIRST_INDEX(expire);} else {int i;uint64_t sz;for (i = 0; i < 4; ++i) {sz = (1ULL << (TVR_BITS + (i+1) * TVN_BITS));if (idx < sz) {idx = NTH_INDEX(expire, i);head = tw->tv[i].vec + idx;break;}}}clinklist_add_back(head, (clinknode_t*)node);
}void timerwheel_add(timerwheel_t *tw, timernode_t *node, uint32_t ticks) {node->expire = tw->currtick + ((ticks > 0) ? ticks : 1);_timerwheel_add(tw, node);
}int timerwheel_del(timerwheel_t *tw, timernode_t *node) {if (!clinklist_is_empty((clinknode_t*)node)) {clinklist_remote((clinknode_t*)node);return 1;}return 0;
}void _timerwheel_cascade(timerwheel_t *tw, tvnum_t *tv, int idx) {clinknode_t head;clinklist_init(&head);clinklist_splice(tv->vec + idx, &head);while (!clinklist_is_empty(&head)) {timernode_t *node = (timernode_t*)head.next;clinklist_remote(head.next);_timerwheel_add(tw, node);}
}void _timerwheel_tick(timerwheel_t *tw) {++tw->currtick;uint32_t currtick = tw->currtick;int index = (currtick & TVR_MASK); if (index == 0) {int i = 0;int idx;do {idx = NTH_INDEX(tw->currtick, i);_timerwheel_cascade(tw, &(tw->tv[i]), idx);} while (idx == 0 && ++i < 4);}clinknode_t head;clinklist_init(&head);clinklist_splice(tw->tvroot.vec + index, &head);while (!clinklist_is_empty(&head)) {timernode_t *node = (timernode_t*)head.next;clinklist_remote(head.next);if (node->callback) {node->callback(node->userdata);}}
}void timerwheel_update(timerwheel_t *tw, uint64_t currtime) {if (currtime > tw->lasttime) {int diff = currtime - tw->lasttime + tw->remainder;int intv = tw->interval;tw->lasttime = currtime;while (diff >= intv) {diff -= intv;_timerwheel_tick(tw);}tw->remainder = diff;}
}

核心函数就两个:

  • _timerwheel_add 把定时器结点加到时间轮里。
  • _timerwheel_tick 指针往前走一步,如果指针到达0,则上一个圈的指针也会跳一步,此时要把上一个圈的链表取出来,重新加到当前圈里(_timerwheel_cascade)

这份代码参考了2.6内核的timer,其中涉及到一些位运算,不过通过上面的讲解,应该不难理解。有兴趣了解的还是看代码吧,代码才是最好的文档:)

打印当前时间 毫秒_时间轮定时器相关推荐

  1. c#获取当前时间 毫秒_《Linux设备驱动程序》(十二)——时间操作(一)

    之前我们学会了如何编写一个字符设备,并对其中的一些重要操作进行了说明.对于一个完整的设备而已,可能还有许多工作要做. 本节我们将要说一下内核中是如何对时间问题进行操作的. 本节主要涉及到以下内容: 内 ...

  2. pwm一个时间单位_时间的换算单位是怎么换算的啊,秒,毫秒,微妙,纳秒等

    展开全部 常见时间单位换算: 1秒=1000毫秒(ms) 1秒=1,000,000 微秒62616964757a686964616fe58685e5aeb931333366303836(μs) 1秒= ...

  3. calendar当前时间整点_时间处理相关类

    时间处理相关类 在计算机世界,我们把1970 年 1 月 1 日 00:00:00定为基准时间,每个度量单位是毫秒(1秒的千分之一). 时间相关类有如下 Date时间类(java.util.Date) ...

  4. 数学建模时间序列分析_时间序列分析建模验证

    数学建模时间序列分析 时间序列预测 (Time Series Forecasting) 背景 (Background) This article is the fourth in the series ...

  5. mysql获取时间_时间类型_时间格式化

    为了方便在数据库中存储日期和时间,MySQL提供了表示日期和时间的数据类型,分别是YEAR.DATE.TIME.DATETIME和TIMESTAMP.如下表列举了这些MySQL中日期和时间数据类型所对 ...

  6. 人生时间计算器_时间计算器

    匿名用户 1级 2010-07-14 回答 展开全部 将下面代码复制到txt文件中,并将其保存为Form1.frm,然后运行 但是要注意:填入的时间不能隔日,即开始时间与结束时间都必须在同一天. VE ...

  7. vue怎么截取时间年月_时间格式的转化 vue与js 年月日 时分秒

    首先使用原生转化的方法 第一种 //时间转换 dateStr(d, sign) { //如果没有传递符号,给一个默认的符号 if (!sign) { sign = '-' } //获取d里面年月日时分 ...

  8. java方法里面能改定时器的时间吗_Kafka 时间轮的原理和实现

    女主宣言 Kafka 作为一个支持实时处理大量请求的分布式流处理平台,需要一个设计良好的定时器来处理异步任务.本文作者将基于 Kafka 1.1.0 版本的源码来介绍 Kafka 中定时器的基础数据结 ...

  9. linux编程之经典多级时间轮定时器(C语言版)

    一. 多级时间轮实现框架 上图是5个时间轮级联的效果图.中间的大轮是工作轮,只有在它上的任务才会被执行:其他轮上的任务时间到后迁移到下一级轮上,他们最终都会迁移到工作轮上而被调度执行. 多级时间轮的原 ...

最新文章

  1. 为什么你的工作经验不值钱
  2. java jni key_JNIKeyProtection
  3. live555 编译 linux,在树莓派上搭建LIVE555 Streaming Media服务器端
  4. nth-child(n)和nth-of-type(n)
  5. 职业生涯起步不要去顶级公司
  6. C#枚举类型的常用操作总结
  7. 谷歌暗示android wear未来或兼容ios系统!腾讯,传谷歌今年5月将推出iOS版本Android Wear...
  8. 谷歌浏览器怎么查找和改变编码格式
  9. AVOD阅读笔记(二):多模型融合RPN----Aggregate View Obeject Detection network
  10. PostGIS 报错libcrypto
  11. Jenkins不能正常trigger
  12. 使用theano进行深度学习实践(一)
  13. 操作系统PV大题_小和尚老和尚喝水问题
  14. 2022年机动车新规,外地人上京牌不需要居住证啦
  15. matlab实现正弦内插算法(低通滤波)
  16. java sha256加密_如何用Sha256进行简单的加密或者解密
  17. json在java代码混淆出问题_代码混淆 GSON完满解决
  18. html网页鼠标样式、css精灵、iconfont、过渡动画笔记
  19. 期货十三篇 第七篇 平仓篇
  20. Vagrant开发环境搭建

热门文章

  1. c# ifram 刷新父页面
  2. 原来JScript中的关键字'var'还是有文章的
  3. 比特币现金诞生一周年,BCH的未来在哪?
  4. Core开发组的傲娇造就了今天的以太坊和比特币现金(BCH)
  5. 微服务系列(七):将单体应用改造为微服务
  6. RHEL和Centos系统的区别?
  7. 《阿里巴巴Java开发规约》插件全球首发!
  8. MongoDB 3.4 复制集全量同步改进
  9. 推荐开发工具系列之--LinrF5(自动刷新)
  10. mysql_config_editor程序的用法