首页 > 其他分享 >定时器组件设计方案

定时器组件设计方案

时间:2024-05-23 21:54:15浏览次数:25  
标签:node 定时器 timer current 设计方案 time 组件 超时

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

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

标签:node,定时器,timer,current,设计方案,time,组件,超时
From: https://blog.csdn.net/2303_77208351/article/details/139158625

相关文章

  • Flutter笔记:Widgets Easier组件库-使用隐私守卫
    Flutter笔记WidgetsEasier组件库:使用隐私守卫-文章信息-Author:李俊才(jcLee95)VisitmeatCSDN:https://jclee95.blog.csdn.netMyWebSite:http://thispage.tech/Email:[email protected]:https://blog.csdn.net......
  • vue3 设置 表单或者页面 懒加载 (或者组件的异步加载)
    用到的工具库  vueUse和useIntersectionObserver1.UseIntersectionObserver函数参数:observerList:由被观察目标所组成的数组,数组项是由React.createRef构建出来的对象callback:当目标元素被曝光所需要触发的函数,该函数接受一个参数indexList,由被曝光元素在observerList......
  • uni-app 样式穿透修改第三方组件 不生效
    问题描述:在uni-app+vue3+less项目中,修改第三方组件的样式使用/deep/ 或者!important都不管用 解决办法: 问题原因:如果你使用的是css,没有使用css预处理器,则可以使用>>>,/deep/,::v-deep。如果你使用的是less或者node-sass,那么可以使用/deep/,::v-deep都可以生效。如果......
  • vant组件库按需加载
    使用 unplugin-vue-components 插件,它可以自动引入组件,并按需引入组件的样式1、安装插件npmiunplugin-vue-components-D2、vite.config.js 文件中配置插件import{fileURLToPath,URL}from'node:url'import{defineConfig}from'vite'importvuefrom'@vit......
  • 开源Blazor UI组件库精选:让你的Blazor项目焕然一新!
    今天给大家推荐一些开源、美观的BlazorUI组件库,这些优秀的开源框架和项目不仅能够帮助开发者们提高开发效率,还能够为他们的项目带来更加丰富的用户体验。注:排名不分先后,都是十分优秀的开源框架和项目​AntDesignBlazorAntDesignBlazor是一个基于Blazor的前端UI组件库,......
  • GeneralUpdate .Net5 WPF、Winfrom、控制台应用自动更新组件
    https://www.bilibili.com/video/BV1aX4y137dd/?vd_source=43d3e66cc2ad46bac54dfb0c6a3a0a23    GeneralUpdate教程2022.4.23 https://www.bilibili.com/video/BV1FT4y1Y7hV/?vd_source=43d3e66cc2ad46bac54dfb0c6a3a0a23   https://mp.weixin.qq.com/s/pR......
  • 全局设置element-ui Dialog组件的close-on-click-modal属性为false
    前言element组件库的Dialog对话框默认可以通过点击modal关闭Dialog,即点击空白处弹框可关闭。属性  importElementUIfrom'element-ui'import'element-ui/lib/theme-chalk/index.css'//默认主题//全局修改默认配置,点击空白处不能关闭弹窗ElementUI.Dialog.......
  • 鸿蒙HarmonyOS实战-Stage模型(AbilityStage组件容器)
    ......
  • 封装 ECharts 为 Vue 组件:X-ECharts 简介
    ECharts是一个广泛使用的开源可视化库,它提供了丰富的图表类型和灵活的配置选项,适用于复杂的数据可视化需求。而X-ECharts是一个基于ECharts封装的Vue组件库,旨在提供更简洁的集成方式,同时兼容Vue2和Vue3,使得开发者能够在不同版本的Vue项目中无缝使用ECharts。Eng......
  • *Unity基础——Transform组件*
    Unity基础——Transform组件一.一些比较重要的点1.首先编辑器面板中的位置信息指的是物体相对于父物体的坐标(本地坐标),如果物体没有父物体,则其父物体可以看作是世界,则该坐标实际上是世界坐标;2.如果在脚本中调用transform组件的position属性,其是一个Vector3类型的对象,指的是......