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

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

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

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

目录
相关文章
|
数据采集 数据可视化 数据挖掘
Python爬虫实战:抓取网站数据并生成报表
本文将介绍如何使用Python编写简单而高效的网络爬虫,从指定的网站上抓取数据,并利用数据分析库生成可视化报表。通过学习本文内容,读者将能够掌握基本的爬虫技术和数据处理方法,为日后开发更复杂的数据采集与分析工具打下坚实基础。
|
开发框架 Ubuntu 应用服务中间件
FastCGI与spawn-fcgi安装与配置
FastCGI与spawn-fcgi安装与配置
1333 0
FastCGI与spawn-fcgi安装与配置
|
算法 数据可视化 机器人
ROS2教程01 ROS2介绍
本文是ROS2(机器人操作系统的下一代)的介绍教程,内容包括ROS2的诞生背景、核心功能、特点、框架以及与ROS1的比较。文章涵盖了ROS2的通信系统、框架和工具、生态系统、全球性社区支持、完全开源、跨平台特性、多机协同能力、实时系统支持和更强的稳定性。此外,还提供了ROS2架构的详细介绍资源链接,适合对ROS2感兴趣的读者学习和了解。
2242 1
|
Java
让星星⭐月亮告诉你,自定义定时器和Java自带原生定时器
定时器是一种可以设置多个具有不同执行时间和间隔的任务的工具。本文介绍了定时器的基本概念、如何自定义实现一个定时器,以及Java原生定时器的使用方法,包括定义定时任务接口、实现任务、定义任务处理线程和使用Java的`Timer`与`TimerTask`类来管理和执行定时任务。
453 3
|
11月前
|
消息中间件 Linux
Linux中的System V通信标准--共享内存、消息队列以及信号量
希望本文能帮助您更好地理解和应用System V IPC机制,构建高效的Linux应用程序。
429 48
|
人工智能 安全
【新手小白】Sora如何申请?Sora使用教程(最新)
近日,OpenAI发布的Sora模型引发了广泛关注,成为行业内外热议的焦点。这一模型不仅在专业领域引起了极大兴趣,还激发了普通群众对于人工通用智能(AGI)的热情。随着Sora模型的爆红,越来越多的人开始关注一个问题:我们何时能开始使用Sora?在OpenAI的官方论坛上,关于这个问题的讨论异常激烈。有关如何使用Sora的话题阅读量已接近70,000,显示出人们对此充满期待。下面这篇文章解答了大家的这些问题。
5846 3
【新手小白】Sora如何申请?Sora使用教程(最新)
|
Python
Python:Pandas实现批量删除Excel中的sheet
Python:Pandas实现批量删除Excel中的sheet
543 0
|
分布式计算 资源调度 NoSQL
|
人工智能 数据可视化 程序员
【推荐100个unity插件之7】使用BehaviorDesigner插件制作BOSS的AI行为树
【推荐100个unity插件之7】使用BehaviorDesigner插件制作BOSS的AI行为树
1487 0
|
存储
CMake中遍历元素的技巧:foreach命令详解
CMake中遍历元素的技巧:foreach命令详解
888 1

热门文章

最新文章