高效定时器设计方案——层级时间轮

2024-05-24 04:52

本文主要是介绍高效定时器设计方案——层级时间轮,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

层级时间轮实现高性能定时器

此篇介绍时间轮,它的时间复杂度是最优的,插入、查找(最小)、删除都是O(1),很恐怖的性能

这里示例一个三层时间轮,模拟时钟表盘的运作方式,便于理解且性能不低

设计思路:

1.根据定时任务的超时时间,按超时时间范围存入不同的链表中,处于同一个链表的任务的超时时间范围相同但无序

2.每一个槽中放入一个链表,可以通过槽访问链表的头尾节点

3.定时任务是否超时的判断依据是,定时任务从创建到即将执行这一过程中,定时器的内部时间time的增长是否大于任务的超时时间,也就是说,在定时器里有内部时间概念,这个时间是由函数调用手动递增的,而不是系统时间

这是定时器以及定时任务的结构

struct timer_node {struct timer_node *next;uint32_t expire;handler_pt callback;uint8_t cancel;
};typedef struct link_list {timer_node_t head;timer_node_t *tail;
} link_list_t;typedef struct timer {link_list_t second[SECONDS]; // 秒槽link_list_t minute[MINUTES]; // 分钟槽link_list_t hour[HOURS]; // 小时槽spinklock_t lock;uint32_t time;time_t current_point;       
} timer_st;

对于一个定时任务 timer_node

1.首先它是一个链表,所以需要next指针

2.expire是自定义的超时时间,这个时间概念是由定时器维护的

3.callback,该定时任务所执行的函数

对于一个定时器 timer

1.second[SECONDS];这是一个结构体数组,数组的每一位存储着一个link_list链表,这个链表存储着一些定时任务节点,根据命名可以看出,SECONDS = 60,

示例:超时时间为1秒的任务存放在second[1]链表中,超时时间为59秒的任务存放在second[59]链表中,此时如果有两个超时时间为2秒的任务,那么它们都将存放在second[2]链表中

2.minute[MINUTES];分钟槽,这里存放着超时时间范围在(1,60]分钟的定时任务

示例:超时时间为65秒和90秒的定时任务都将存放在minute[1]链表中,因为它们都属于60-120s这个时间范围

3.time,这就是定时器维护的内部时间了,一般初始化为0,代表第0秒,在时间轮运行时,会有函数将它递增,因此它区别于系统时间每秒+1,它的秒数增长频率是不固定的

4.time_t current_point;这是一个time_t类型的变量,用于保存一个系统时间,在时间轮中用于time增长的参考

接下来介绍几个核心的函数:

1.添加定时任务:

static void add_node(timer_st *T, timer_node_t *node) {uint32_t time = node->expire;uint32_t current_time = T->time;uint32_t mesc = time - current_time;if (mesc < ONE_MINUTE) {link_to(&T->second[time % SECONDS], node);} else if (mesc < ONE_HOUR) {link_to(&T->minute[(uint32_t)(time/ONE_MINUTE) % MINUTES], node);} else {link_to(&T->hour[(uint32_t)(time/ONE_HOUR) % HOURS], node);}
}timer_node_t *add_timer(int time, handler_pt func) {timer_node_t *node = (timer_node_t *)malloc(sizeof(*node));spinlock_lock(&TI->lock);node->expire = time + TI->time;ptinrf("add timer at %u, expire at %u, now_time at %lu\n", TI->time, node->expire, now_time());node->callback = func;node->cancel = 0;if (time <= 0) {spinlock_unlock(&TI-> lock);node->callback(node);free(node);return NULL;}add_node(TI, node);spinlock_unlock(&TI->lock);return node;
}

逻辑:

1.先根据任务指定的expire确定超时时间点,为expire(超时时间) + TI->time(定时器当前时间)

  1. 设置任务的回调函数(非重点)

  2. 根据超时时间点,将该定时任务添加到正确的时间轮槽中:

    static void add_node(timer_st *T, timer_node_t *node) {uint32_t time = node->expire;  // 相对超时时间uint32_t current_time = T->time;  // 当前的定时器事件uint32_t mesc = time - current_time; // 多少秒后超时(绝对超时时间)if (mesc < ONE_MINUTE) { // 绝对超时时间小于一分钟link_to(&T->second[time % SECONDS], node); // 添加到秒槽中} else if (mesc < ONE_HOUR) { // 大于一分钟,小于60分钟link_to(&T->minute[(uint32_t)(time/ONE_MINUTE) % MINUTES], node);// 添加到分钟槽中} else { // 添加到小时槽中link_to(&T->hour[(uint32_t)(time/ONE_HOUR) % HOURS], node);}
    }
    

重点中的重点

相信你注意到了add_node函数中的**mesc = time - current_time;**,由于定时器的时间推进,一个定时任务的绝对超时时间会随之减少,会导致在某一(定时器)时刻,一些定时任务的位置变得不正确,例如一个65秒的定时任务,在10秒后仍未得到处理,那么它此时的绝对超时时间是55秒,这时,它应该由原来所在的minute分钟槽移动到second秒槽中

由于这种情况会普遍发生,我们需要利用额外的函数处理这些需要重新换槽的任务——remap函数

remap()// 重新映射

remap要做的很简单:将一个槽中的全部或部分节点搬到另一个或几个槽中,简洁的操作是:先将原槽清空,再为这些节点重新匹配合适的槽,这就叫做重新映射

static void remap(timer_st *T, link_list_t *level, int idx) {timer_node_t *current = link_clear(&level[idx]); // 清空当前槽while (current) {  // 将槽中的节点全部重新映射到新槽timer_node_t *temp = current->next;add_node(T, current); // 核心操作,重新匹配并添加到槽中current = temp;}
}

时间轮的推进—定时器内部时间增长

static void
timer_shift(timer_st *T) {uint32_t ct = ++T->time % HALF_DAY;  //  定时器内部时间 + 1秒if (ct % SECONDS == 0) {     // 当前时间为整分钟// 每分钟重新分配一次uint32_t minute_idx = (ct / ONE_MINUTE) % MINUTES;if (minute_idx != 0) {  // 当前时间是整分钟remap(T, T->minute, minute_idx);}// 每小时重新分配一次if (ct % ONE_HOUR == 0) {uint32_t hour_idx = (ct / ONE_HOUR) % HOURS;remap(T, T->hour, hour_idx);}}
}

每推进一秒定时器时间,判断一次是否需要重新分配分钟槽或小时槽

1.每一分钟remap一次分钟槽到秒槽,因为每过一分钟,大于一分钟小于两分钟的任务的绝对超时时间会变为一分钟内

2.每小时remap一次小时槽到分钟槽中,因为每过一小时,大一小时小于两小时的任务的绝对时间会变为一小时内

执行定时任务:

static void timer_execute(timer_st *T) {uint32_t idx = T->time % SECONDS;   // 每一次执行最小时间单位槽-->秒 中的定时器任务while (T->second[idx].head.next) {timer_node_t *current = link_clear(&T->second[idx]);spinklock_unlock(&T->lock);dispatch_list(current);spinlock_lock(&T->lock);}
}static void dispath_list(timer_node_t *current) {do {timer_node_t * temp = current;current = current->next;if (temp->cancel == 0)temp->callback(temp);free(temp);} while (current);
}

每次执行一个槽中链表的所有任务,任务执行后会被移除

值得注意的是

每次只执行秒槽中的任务,因为这是定时器的最小执行精度,并且分钟槽和小时槽中的任务最终也会随定时器的时间推进而重新映射到秒槽中

运行定时器

static void timer_update(timer_st *T) {spinlock_lock(&T->lock);timer_execute(T); // 执行一个秒槽中的任务timer_shift(T);  // 推进定时器内部时间timer_execute(T); spinlock_unlock(&T->lock);
}void check_timer(int *stop) {  //  同步系统时间和定时器的当前时间while (*stop == 0) {    time_t cp = now_time();   // 获取系统当前时间if (cp != TI->current_point) {  // 当前系统时间于上一次获取的系统时间的对比uint32_t diff = (uint32_t)(cp - TI->current_point); // 当前系统时间于上一次获取的系统时间的时间差TI->current_point = cp;  // 更新定时器内暂存的系统时间int i;for (i = 0; i < diff; i++) {  // 推进定时器,补偿时间差timer_update(TI);  // 推动定时器时间增长、处理任务}}usleep(200000); // 循环运行间隔}
}

推荐学习 https://xxetb.xetslk.com/s/p5Ibb

这篇关于高效定时器设计方案——层级时间轮的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/997428

相关文章

C#使用SQLite进行大数据量高效处理的代码示例

《C#使用SQLite进行大数据量高效处理的代码示例》在软件开发中,高效处理大数据量是一个常见且具有挑战性的任务,SQLite因其零配置、嵌入式、跨平台的特性,成为许多开发者的首选数据库,本文将深入探... 目录前言准备工作数据实体核心技术批量插入:从乌龟到猎豹的蜕变分页查询:加载百万数据异步处理:拒绝界面

Spring Boot + MyBatis Plus 高效开发实战从入门到进阶优化(推荐)

《SpringBoot+MyBatisPlus高效开发实战从入门到进阶优化(推荐)》本文将详细介绍SpringBoot+MyBatisPlus的完整开发流程,并深入剖析分页查询、批量操作、动... 目录Spring Boot + MyBATis Plus 高效开发实战:从入门到进阶优化1. MyBatis

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义

SpringBoot使用OkHttp完成高效网络请求详解

《SpringBoot使用OkHttp完成高效网络请求详解》OkHttp是一个高效的HTTP客户端,支持同步和异步请求,且具备自动处理cookie、缓存和连接池等高级功能,下面我们来看看SpringB... 目录一、OkHttp 简介二、在 Spring Boot 中集成 OkHttp三、封装 OkHttp

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

使用Python高效获取网络数据的操作指南

《使用Python高效获取网络数据的操作指南》网络爬虫是一种自动化程序,用于访问和提取网站上的数据,Python是进行网络爬虫开发的理想语言,拥有丰富的库和工具,使得编写和维护爬虫变得简单高效,本文将... 目录网络爬虫的基本概念常用库介绍安装库Requests和BeautifulSoup爬虫开发发送请求解

Springboot如何配置Scheduler定时器

《Springboot如何配置Scheduler定时器》:本文主要介绍Springboot如何配置Scheduler定时器问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录Springboot配置Scheduler定时器1.在启动类上添加 @EnableSchedulin

Python如何获取域名的SSL证书信息和到期时间

《Python如何获取域名的SSL证书信息和到期时间》在当今互联网时代,SSL证书的重要性不言而喻,它不仅为用户提供了安全的连接,还能提高网站的搜索引擎排名,那我们怎么才能通过Python获取域名的S... 目录了解SSL证书的基本概念使用python库来抓取SSL证书信息安装必要的库编写获取SSL证书信息

MySQL 日期时间格式化函数 DATE_FORMAT() 的使用示例详解

《MySQL日期时间格式化函数DATE_FORMAT()的使用示例详解》`DATE_FORMAT()`是MySQL中用于格式化日期时间的函数,本文详细介绍了其语法、格式化字符串的含义以及常见日期... 目录一、DATE_FORMAT()语法二、格式化字符串详解三、常见日期时间格式组合四、业务场景五、总结一、

C++实现回文串判断的两种高效方法

《C++实现回文串判断的两种高效方法》文章介绍了两种判断回文串的方法:解法一通过创建新字符串来处理,解法二在原字符串上直接筛选判断,两种方法都使用了双指针法,文中通过代码示例讲解的非常详细,需要的朋友... 目录一、问题描述示例二、解法一:将字母数字连接到新的 string思路代码实现代码解释复杂度分析三、