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

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

相关文章

go中的时间处理过程

《go中的时间处理过程》:本文主要介绍go中的时间处理过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 获取当前时间2 获取当前时间戳3 获取当前时间的字符串格式4 相互转化4.1 时间戳转时间字符串 (int64 > string)4.2 时间字符串转时间

Golang如何对cron进行二次封装实现指定时间执行定时任务

《Golang如何对cron进行二次封装实现指定时间执行定时任务》:本文主要介绍Golang如何对cron进行二次封装实现指定时间执行定时任务问题,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录背景cron库下载代码示例【1】结构体定义【2】定时任务开启【3】使用示例【4】控制台输出总结背景

在Golang中实现定时任务的几种高效方法

《在Golang中实现定时任务的几种高效方法》本文将详细介绍在Golang中实现定时任务的几种高效方法,包括time包中的Ticker和Timer、第三方库cron的使用,以及基于channel和go... 目录背景介绍目的和范围预期读者文档结构概述术语表核心概念与联系故事引入核心概念解释核心概念之间的关系

SpringMVC高效获取JavaBean对象指南

《SpringMVC高效获取JavaBean对象指南》SpringMVC通过数据绑定自动将请求参数映射到JavaBean,支持表单、URL及JSON数据,需用@ModelAttribute、@Requ... 目录Spring MVC 获取 JavaBean 对象指南核心机制:数据绑定实现步骤1. 定义 Ja

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

C++ 函数 strftime 和时间格式示例详解

《C++函数strftime和时间格式示例详解》strftime是C/C++标准库中用于格式化日期和时间的函数,定义在ctime头文件中,它将tm结构体中的时间信息转换为指定格式的字符串,是处理... 目录C++ 函数 strftipythonme 详解一、函数原型二、功能描述三、格式字符串说明四、返回值五

从基础到进阶详解Pandas时间数据处理指南

《从基础到进阶详解Pandas时间数据处理指南》Pandas构建了完整的时间数据处理生态,核心由四个基础类构成,Timestamp,DatetimeIndex,Period和Timedelta,下面我... 目录1. 时间数据类型与基础操作1.1 核心时间对象体系1.2 时间数据生成技巧2. 时间索引与数据

Python基于微信OCR引擎实现高效图片文字识别

《Python基于微信OCR引擎实现高效图片文字识别》这篇文章主要为大家详细介绍了一款基于微信OCR引擎的图片文字识别桌面应用开发全过程,可以实现从图片拖拽识别到文字提取,感兴趣的小伙伴可以跟随小编一... 目录一、项目概述1.1 开发背景1.2 技术选型1.3 核心优势二、功能详解2.1 核心功能模块2.

基于Python构建一个高效词汇表

《基于Python构建一个高效词汇表》在自然语言处理(NLP)领域,构建高效的词汇表是文本预处理的关键步骤,本文将解析一个使用Python实现的n-gram词频统计工具,感兴趣的可以了解下... 目录一、项目背景与目标1.1 技术需求1.2 核心技术栈二、核心代码解析2.1 数据处理函数2.2 数据处理流程

Python中bisect_left 函数实现高效插入与有序列表管理

《Python中bisect_left函数实现高效插入与有序列表管理》Python的bisect_left函数通过二分查找高效定位有序列表插入位置,与bisect_right的区别在于处理重复元素时... 目录一、bisect_left 基本介绍1.1 函数定义1.2 核心功能二、bisect_left 与