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

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

相关文章

如何利用Java获取当天的开始和结束时间

《如何利用Java获取当天的开始和结束时间》:本文主要介绍如何使用Java8的LocalDate和LocalDateTime类获取指定日期的开始和结束时间,展示了如何通过这些类进行日期和时间的处... 目录前言1. Java日期时间API概述2. 获取当天的开始和结束时间代码解析运行结果3. 总结前言在J

轻松上手MYSQL之JSON函数实现高效数据查询与操作

《轻松上手MYSQL之JSON函数实现高效数据查询与操作》:本文主要介绍轻松上手MYSQL之JSON函数实现高效数据查询与操作的相关资料,MySQL提供了多个JSON函数,用于处理和查询JSON数... 目录一、jsON_EXTRACT 提取指定数据二、JSON_UNQUOTE 取消双引号三、JSON_KE

修改若依框架Token的过期时间问题

《修改若依框架Token的过期时间问题》本文介绍了如何修改若依框架中Token的过期时间,通过修改`application.yml`文件中的配置来实现,默认单位为分钟,希望此经验对大家有所帮助,也欢迎... 目录修改若依框架Token的过期时间修改Token的过期时间关闭Token的过期时js间总结修改若依

Go Mongox轻松实现MongoDB的时间字段自动填充

《GoMongox轻松实现MongoDB的时间字段自动填充》这篇文章主要为大家详细介绍了Go语言如何使用mongox库,在插入和更新数据时自动填充时间字段,从而提升开发效率并减少重复代码,需要的可以... 目录前言时间字段填充规则Mongox 的安装使用 Mongox 进行插入操作使用 Mongox 进行更

Tomcat高效部署与性能优化方式

《Tomcat高效部署与性能优化方式》本文介绍了如何高效部署Tomcat并进行性能优化,以确保Web应用的稳定运行和高效响应,高效部署包括环境准备、安装Tomcat、配置Tomcat、部署应用和启动T... 目录Tomcat高效部署与性能优化一、引言二、Tomcat高效部署三、Tomcat性能优化总结Tom

Python利用自带模块实现屏幕像素高效操作

《Python利用自带模块实现屏幕像素高效操作》这篇文章主要为大家详细介绍了Python如何利用自带模块实现屏幕像素高效操作,文中的示例代码讲解详,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1、获取屏幕放缩比例2、获取屏幕指定坐标处像素颜色3、一个简单的使用案例4、总结1、获取屏幕放缩比例from

对postgresql日期和时间的比较

《对postgresql日期和时间的比较》文章介绍了在数据库中处理日期和时间类型时的一些注意事项,包括如何将字符串转换为日期或时间类型,以及在比较时自动转换的情况,作者建议在使用数据库时,根据具体情况... 目录PostgreSQL日期和时间比较DB里保存到时分秒,需要和年月日比较db里存储date或者ti

使用Python实现高效的端口扫描器

《使用Python实现高效的端口扫描器》在网络安全领域,端口扫描是一项基本而重要的技能,通过端口扫描,可以发现目标主机上开放的服务和端口,这对于安全评估、渗透测试等有着不可忽视的作用,本文将介绍如何使... 目录1. 端口扫描的基本原理2. 使用python实现端口扫描2.1 安装必要的库2.2 编写端口扫

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

在C#中获取端口号与系统信息的高效实践

《在C#中获取端口号与系统信息的高效实践》在现代软件开发中,尤其是系统管理、运维、监控和性能优化等场景中,了解计算机硬件和网络的状态至关重要,C#作为一种广泛应用的编程语言,提供了丰富的API来帮助开... 目录引言1. 获取端口号信息1.1 获取活动的 TCP 和 UDP 连接说明:应用场景:2. 获取硬