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

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

此篇介绍时间轮,它的时间复杂度是最优的,插入、查找(最小)、删除都是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.mzph.cn/news/839096.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

力扣爆刷第142天之二叉树五连刷(构造树、搜索树)

力扣爆刷第142天之二叉树五连刷&#xff08;构造树、搜索树&#xff09; 文章目录 力扣爆刷第142天之二叉树五连刷&#xff08;构造树、搜索树&#xff09;一、106. 从中序与后序遍历序列构造二叉树二、654. 最大二叉树三、617. 合并二叉树四、700. 二叉搜索树中的搜索五、98. …

卷积神经网络(CNN)详细介绍及其原理详解

卷积神经网络&#xff08;Convolutional Neural Networks&#xff0c;简称CNN&#xff09;是深度学习中非常重要的一类神经网络&#xff0c;主要用于图像识别、图像分类、物体检测等计算机视觉任务。本文将详细介绍卷积神经网络的基本概念、结构组成及其工作原理&#xff0c;并…

每日一题Reverse Card (Easy Version)

文章目录 题名&#xff1a;题意&#xff1a;题解&#xff1a;代码&#xff1a; 题名&#xff1a; Reverse Card (Easy Version) 题意&#xff1a; 给定 n n n&#xff0c; m m m&#xff0c;存在 1 < a < n 1<a<n 1<a<n&#xff0c; 1 < b < m …

什么是网关鉴权及其在Spring Cloud Gateway中的实现

在现代微服务架构中&#xff0c;网关扮演着非常重要的角色&#xff0c;它是系统和外部世界之间的入口&#xff0c;负责路由请求、流量控制以及安全保护等任务。其中&#xff0c;网关鉴权是保障系统安全的重要环节之一。本文将深入介绍什么是网关鉴权&#xff0c;以及如何通过过…

BCD编码(8421)介绍

概念 BCD (Binary-Coded Decimal) 是一种二进制的数字编码形式&#xff0c;其特点每个十进制数位用4个二进制位来表示。 在网络IO中&#xff0c;你传输一个数字类型最少需要一字节&#xff0c;传输两个数字类型最少需要两字节&#xff0c;但是当你使用BCD编码后传输&#xff…

防静电液的这些用处你知道多少

防静电液又叫抗静电剂&#xff0c;是工业上常用来消除静电的化学用品&#xff0c;一般是液体状态&#xff0c;它的用途很广泛。 防静电液适用于对静电有控制要求的电器、仪器桌面、台面、塑料制品、包装品、存储盒、托盘、毛毯、织物等任何物品表面。 应用举例如消除各种塑胶材…

微服务中的鉴权怎么做?

大家好&#xff0c;我是苍何呀。 现在出去找工作&#xff0c;简历上不写上微服务的技术&#xff0c;仿佛自己跟不上时代了&#xff0c;面试官更是喜欢盯着微服务项目来提问。 但其实虽说微服务是主流&#xff0c;随着云原生架构的发展&#xff0c;微服务也是趋势&#xff0c;…

图论-最短路算法

1. Floyd算法 作用&#xff1a;用于求解多源最短路&#xff0c;可以求解出任意两点的最短路 利用动态规划只需三重循环即可&#xff08;动态规划可以把问题求解分为多个阶段&#xff09;定义dp[k][i][j]表示点i到点j的路径&#xff08;除去起点终点&#xff09;中最大编号不超…

算法题【面试准备】

算法题【面试准备】 前言推荐算法题NC72二叉树的镜像NC8:二叉树根节点到叶子节点和为指定值的路径NC9二叉树中是否存在节点和为指定值的路径NC14二叉树的之字形层序遍历[NC15 求二叉树的层序遍历](https://www.nowcoder.com/study/live/689/2/69)NC37:合并区间[NC111 最大数](h…

AI模型发展之路:开源还是闭源?

AI模型发展之路&#xff1a;开源还是闭源&#xff1f; 开源路径优势&#xff1a;发展前景&#xff1a; 闭源路径优势&#xff1a;发展前景&#xff1a; 一、自动化内容生成文章生成技术原理应用场景 视频生成技术原理应用场景 音频生成技术原理应用场景 二、内容分发与推广智能…

leetcode725-Split Linked List in Parts

题目 给你一个头结点为 head 的单链表和一个整数 k &#xff0c;请你设计一个算法将链表分隔为 k 个连续的部分。 每部分的长度应该尽可能的相等&#xff1a;任意两部分的长度差距不能超过 1 。这可能会导致有些部分为 null 。 这 k 个部分应该按照在链表中出现的顺序排列&…

数据库的约束 not null, unique, default, primary key, foreign key, check

约束可以理解成 数据库提供的一种针对数据的合法性进行验证的机制, 在创建表的时候使用 1. 约束类型 NOT NULL - 指示某列不能存储 NULL 值, 表里的这个内容是必填项UNIQUE - 保证某列的每行必须有唯一的值, 不能重复 每次插入/修改时, 都要先触发查询, 如果当前插入/修改的…

原来Rstudio还可以这么使用,又方便了一些

在别人的电子书&#xff0c;你的电子书&#xff0c;都在bookdown中我们讲述了bookdown用于自动化文档生成。里面涉及到一个文件Rproj用于项目管理。 本身是一个很简单的文件&#xff0c;里面的内容一般不需要修改&#xff0c;只是放置在每个项目目录下即可。 比如我们有个内容…

C语言-牛客-实现四舍五入

欢迎来到Harper.Lee的学习小世界&#xff01; 博主主页传送门&#xff1a;Harper.Lee的博客主页 想要一起进步的uu欢迎来后台找我哦&#xff01; 本篇博客总结C语言刷题的相关笔记~~~~ #牛客–实现四舍五入 题目描述&#xff1a;随机输入浮点数&#xff0c;输出四舍五入后的整数…

数据链路层简单介绍

mac地址&#xff08;物理地址&#xff09; mac地址和ip地址&#xff0c;目的都是为了区分网络上的不同设备的&#xff0c;在最开始的时候&#xff0c;mac地址和ip地址是两伙人&#xff0c;独立各自提出的&#xff0c;ip地址是4个字节&#xff08;早都不够用了&#xff09;&…

OFDM 802.11a的FPGA实现(二十一)发射主控模块MCU(含代码)

目录 1.前言 2.主控逻辑 3.Matlab 4.verilog 5.ModelSim 6.ModelSim仿真结构与Matlab自动化对比 完整工程链接&#xff08;含verilog和Matlab代码&#xff09;&#xff1a; https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzkxNjM0NDk2Nw&actiongetalbum&album…

【sass嵌套规则 (Nested Rules)】

Sass的嵌套规则&#xff08;Nested Rules&#xff09;是一个非常重要的功能&#xff0c;它允许你在Sass文件中嵌套CSS规则集&#xff0c;从而以更直观、更结构化的方式编写样式。下面我将详细解释Sass嵌套规则的概念、作用、使用场景以及示例代码。 定义和用途 Sass嵌套规则允…

Qt 模态 非模态的使用

Qt 模态 非模态的使用 在Qt中&#xff0c;模态&#xff08;modal&#xff09;和非模态&#xff08;modeless&#xff09;对话框之间有几个重要区别&#xff1a; 模态对话框&#xff1a; 模态对话框会阻止用户与其它应用程序窗口进行交互&#xff0c;直到对话框被关闭为止&#…

FastAPI+Nuxt单域名部署实践:无需子域名的前后端分离解决方案

FastAPINuxt单域名部署实践&#xff1a;无需子域名的前后端分离解决方案 注&#xff1a;此博客写于2024年5月23日。FastAPI已经到0.111.0 版本了。 背景历史 上一个接手网站的人不管了&#xff1a;Wordpress —重写–> Vue 发现Vue做SEO优化很麻烦&#xff1a;Vue —重构…

Spring6笔记(五):国际化、数据校验、提前编译

九、国际化&#xff1a;i18n 9.1 i18n概述 9.2 Java国际化 9.3 Spring6国际化 十、数据校验&#xff1a;Validation 10.1 Spring Validation 概述 10.2 实验一&#xff1a;通过 validator 接口实现 10.3 实验三&#xff1a;Bean Validation 注解 10.4 实验四&#xff1a;实现…