定时器方案:时间表盘

news2024/9/22 22:38:29

目录

一:前言

二:手搓时间表盘

1、任务结点,层级,表盘的结构体

2、表盘的初始化

3、添加定时任务

4、删除定时任务

5、检查任务是否超时

6、清空任务


一:前言

我之前有两篇文章是写定时器方案的,大家可以看一看,这篇文章是基于之前的文章写出来的。

二:手搓时间表盘

1、任务结点,层级,表盘的结构体

对于时间表盘,通过之前的讲解,我们可以得知,它类似于钟表的结构,有时分秒以及指针。

表盘:整个时间表盘的结构,它包含层级和任务结点,其中含有三个层级:时分秒。这些层级使用数组表示。还有锁和当前的时间。

层级:层级使用数组表示了,那么他的每个坑位使用一个指针指向这个地址。

任务结点:这些结点包含当前的时间和任务。

struct timer_node {         //表盘上一个一个的结点
    struct timer_node *next;    //指向下一个
    uint32_t expire;            //时间
    handler_pt callback;        //回调函数
    uint8_t cancel;             //是否被删除
}timer_node_t;

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];
    spinlock_t lock;                //自旋锁
    uint32_t time;                  //时间
    time_t current_point;           //当前的时间
}timer_st;

2、表盘的初始化

首先就是要开辟成表盘结构体的内存,然后通过link_clear将每个层级的每个坑位进行初始化操作。大家如果想象不到这个图的话,那就看看这个。

//初始化表盘
void
init_timer(void) {
    TI = create_timer();
    TI->current_point = now_time();
}

//创建表盘
static timer_st *
create_timer() {
    timer_st *r = (timer_st *)malloc(sizeof(timer_st));     //为表盘开辟空间
    memset(r,0,sizeof(*r));

    //下面是进行对时分秒行的初始化,在每个表盘层级的数组坑位中添加一个可以连一串结点的头节点。
    int i;
    for (i=0; i<SECONDS; i++) {
        link_clear(&r->second[i]);
    }
    for (i=0; i<MINUTES; i++) {
        link_clear(&r->minute[i]);
    }
    for (i=0; i<HOURS; i++) {
        link_clear(&r->hour[i]);
    }
    spinlock_init(&r->lock);                //初始化完之后进行初始化这个自旋锁。
    return r;
}

time_t
now_time() {
    struct timespec ti;
    clock_gettime(CLOCK_MONOTONIC, &ti);
    // 1ns = 1/1000000000 s = 1/1000000 ms
    return ti.tv_sec;
}

static timer_node_t *
link_clear(link_list_t *list) {
    timer_node_t * ret = list->head.next;       //创建个结点,复制第一个结点
    list->head.next = 0;
    list->tail = &(list->head);                 //将尾指针也指向这里。

    return ret;
}

3、添加定时任务

        这个添加定时任务的整体就是,先创建一个任务结点,将这个任务节点进行初始化,如添加时间,添加回调函数。紧接着就是判断时间与当前的时间,如果时间是已经过去的时间,那么我们就立刻让此任务执行。

        然而当任务是未来要发生的,那么就要将这个创建好的结点进行插入操作。从图中看的话也就是将这个结点先找到所在的层级,然后所在的位置,在最后连接到一连串的结点中去。

//添加时间任务结点
//整体是:创建时间结点,加锁,对这个结点初始化(时间,回调函数),判断时间,时间为负立即执行,时间为正,则添加到行中去。
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;       //下面就是存放时间和回调函数
    printf("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时30分30秒,那么它要先存放到1小时的位置,等到时间指针移动到小时这里,那么我们检查这个小时这个结点\
我们要将这个结点进行删除一小时,然后发现时间为30分30秒,我们又重新调整位置,把这个结点取出来,存放到分的层级,以此类推。
static void
add_node(timer_st *T, timer_node_t *node) { //传入哪个表盘的哪个结点
    uint32_t time=node->expire;
    uint32_t current_time=T->time;
    uint32_t msec = time - current_time;        //这里表示间隔的时间,也就是多少时间后触发:6541秒后触发
    if (msec < ONE_MINUTE) {                    //下面的link_to是将新添加的结点存放到层级中去,也就是连接到指定位置的后面
        link_to(&T->second[time % SECONDS], node);          //这里是秒的行层
    } else if (msec < 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);                 //时的层级
    }   
    //也就是根据多长时间后触发,根据这个时间,把他存放在哪一层。
}

//将结点连接起来,每次都添加到结点的后面
static void
link_to(link_list_t *list, timer_node_t *node) {    //他这里传入的是层级中的一块位置,添加到这个位置的后面。
    list->tail->next = node;        //挪动尾指针,一直指向行层的末尾。
    list->tail = node;
    node->next=0;       //新结点的后面为空。
}

4、删除定时任务

        这里的删除定时任务很简单,就是将他的cancel值设置为1。它并不是让这个结点消失,如果删除这个结点的话,我们就要按着正常的删除结点的步骤写,很麻烦,如果这样的话,写起来很简单。

//设置他的cancel值,将他默认为被删除
void
del_timer(timer_node_t *node) {
    node->cancel = 1;
}

5、检查任务是否超时

        讲一下这里的流程:当我们执行这个检查的函数时,我们首先会获得时间差,这里的时间差是通过下面休眠得来的。时间差是两秒,至于为什么是两秒,往后看就明白了,这里先记住。

        由于时间差是两秒,那么这个循环也就是循环两次,进入这个timer_update函数后。通过execute函数执行超时的任务,在里面我们可以得到秒数,我们通过这个秒数就可以判断当前的时间是几秒,然后通过找到第几秒的位置。通过link_clear函数得到这一连串任务结点的头部 。这一连串的任务结点就是同一时刻的全部任务,我们通过这个头部,可以完成这一连串的任务,并且释放掉。

        这一秒的任务已经执行完了,那么通过timer_shift中将时间进行++操作,我们就得到下一秒的时间了,时间移动了,那么我们要更新整个时间表盘,也就是重新映射,为什么呢?如果加这一秒正好让时针移动,那么这一小时内的全部任务要向上映射,全部存储到分层级中去。如果是分针移动,那么分的要向上映射到秒中去。

        这里的重新映射也就是将任务结点所在的位置进行调整,将他们全部拿出来然后再重新插入就好了。如果看不懂,可以去看我之前的文章。这里主要是怎么实现。

//检查时间是否超时
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); //为什么要休眠呢?这里休眠会给前面造成时间差,时间差也就是这里的休眠时长:两秒。那为什么两秒呢?\
        上面timer_update中我们执行了两次,其中还加了一秒。\
        如果当前时间为4秒,那我们通过timer_update会直接执行第四秒的任务,并重新映射,然后执行第五秒的任务,然后再循环一次,到第6秒(循环两次)\
        从4秒到了6秒,那我们再次走timer_update,会发现直接再次执行第6秒,这样并没有漏掉时间或者多走时间。\
        并且再次执行第6秒是为了避免这个时候又突然加入这一时刻的任务。          
    }
}

//时间表盘的更新,重新对时间任务进行映射。
static void
timer_update(timer_st *T) {
    spinlock_lock(&T->lock);        //表盘的更新操作要加锁。
    timer_execute(T);               //定时任务的执行
    timer_shift(T);                 //重新映射完
    timer_execute(T);               //再次进行执行的操作,因为在上面重新映射的时候,将时间+1了,所以这一秒也需要进行执行。
    spinlock_unlock(&T->lock);
}

//定时任务的执行,拿出坑位中全部的任务
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]);   //因此我们需要将这一个坑位中全部的任务进行处理,也就是通过link_clear拿出全部的结点。
        spinlock_unlock(&T->lock);
        dispatch_list(current);                 //将这里面全部的任务进行执行。
        spinlock_lock(&T->lock);
    }
}

//处理任务,将拿出的任务全部进行处理
static void
dispatch_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_shift(timer_st *T) {
    //因为咱们存储的时是12小时,对于下午几点的时间要进行单独的设置,比如这里,通过加一,然后对半天的时间取余操作,就得到了下午的时间。
    //下面的操作就是重新映射所在的位置。时间在增加,每次根据增加的时间,来进行重新映射所在的位置。
    uint32_t ct = ++T->time % HALF_DAY;     //++后得到下一秒的时间,因此需要再次执行一遍timer_execute。       
    if (ct == 0) {                  //如果正好是12点整,那么就存放到小时的第0号位置。
        remap(T, T->hour, 0);
    } 
    else {                                          //其他时间
        if (ct % SECONDS == 0) {                    //秒是最上面的层级,不需要进行调整,
            {
                uint32_t idx = (uint32_t)(ct / ONE_MINUTE) % MINUTES;
                if (idx != 0) {
                    remap(T, T->minute, idx);
                    return;
                }
            }
            {
                uint32_t idx = (uint32_t)(ct / ONE_HOUR) % HOURS;
                if (idx != 0) {
                    remap(T, T->hour, idx);
                }
            }
        }
    }
}

6、清空任务

这个清空也很简单,就是将每个坑位全部提取出来然后释放掉就可以。

//将全部的任务结点进行删除
void
clear_timer() {
    int i;
    for (i=0; i<SECONDS; i++) {                 //遍历整个层级
        link_list_t * list = &TI->second[i];    //获得层级中的每一个坑位
        timer_node_t* current = list->head.next;
        while(current) {                        //通过循环将这个坑位中的一串任务结点全部清空。
            timer_node_t * temp = current;
            current = current->next;
            free(temp);
        }
        link_clear(&TI->second[i]);         //清空任务结点
    }                                               //下面也是一样
    for (i=0; i<MINUTES; i++) {
        link_list_t * list = &TI->minute[i];
        timer_node_t* current = list->head.next;
        while(current) {
            timer_node_t * temp = current;
            current = current->next;
            free(temp);
        }
        link_clear(&TI->minute[i]);
    }
    for (i=0; i<HOURS; i++) {
        link_list_t * list = &TI->hour[i];
        timer_node_t* current = list->head.next;
        while(current) {
            timer_node_t * temp = current;
            current = current->next;
            free(temp);
        }
        link_clear(&TI->hour[i]);
    }
}

今天的讲解结束了,看不懂的可以去先看之前的文章,将大概梳理清楚,然后再来看这篇实现文章。https://xxetb.xetslk.com/s/2D96kH

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2094224.html

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

相关文章

智菜谱推|基于SprinBoot+vue的智能菜谱推荐系统(源码+数据库+文档)

智能菜谱推荐系统 基于SprinBootvue的智能菜谱推荐系统 一、前言 二、系统设计 三、系统功能设计 系统功能实现 管理员功能模块实现 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂…

【开源免费】基于SpringBoot+Vue.JS渔具租赁系统(JAVA毕业设计)

本文项目编号 T 005 &#xff0c;文末自助获取源码 \color{red}{T005&#xff0c;文末自助获取源码} T005&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 渔…

低空经济概念火爆:无人机飞手人才培养先行

随着科技的飞速发展&#xff0c;低空经济作为新兴的经济形态&#xff0c;正以前所未有的速度崛起&#xff0c;成为推动产业升级和经济发展的新引擎。无人机作为低空经济的重要组成部分&#xff0c;其应用领域已从最初的军事侦察、航拍扩展到农业植保、物流配送、环境监测、应急…

使用corrplot绘制行、列不同,且带有p值显著性标注的相关系数图

导读&#xff1a; 相关系数衡量两个变量之间的线性关系&#xff0c;通常以N*N的矩阵形式展示。例如样品vs样品&#xff0c;或者基因vs基因的相关性。本文介绍了使用corrplot R包绘制M*N的相关系数矩阵&#xff0c;例如M个基因表达与N个代谢物信号间的相关性&#xff0c;同时带…

国产芯片+国产操作系统打造办公系统

在《使用国产操作系统作为开发系统》一文中&#xff0c;我介绍了将开发系统从 Ubuntu 替换为 Deepin 系统的过程。经过一个多月的使用&#xff0c;Deepin 系统已然成为我的主力开发平台&#xff0c;其顺手程度让我对国产操作系统的信心大增。于是&#xff0c;我开始将目光瞄向公…

顶级开源许可证详解

目录 软件许可证类型&#xff1a;版权左派和宽容型 顶级开源许可证详解 GNU 通用公共许可证 (GPL) Apache 许可证 Microsoft 公共许可证 (Ms-PL) 伯克利软件发行版 (BSD) 通用开发和分发许可证 (CDDL) Eclipse 公共许可证 (EPL) MIT 许可证 了解你的开源许可证&#…

java编辑器——IntelliJ IDEA

java编辑器有两种选择——IntelliJ IDEA和VsCode。其中IntelliJ IDEA现在是企业用的比较多的&#xff0c;是专门为java设计的&#xff0c;而VsCode则是通过插件来实现Java编辑的。 1.IntelliJ IDEA 官网下载链接&#xff1a;https://www.jetbrains.com/idea/ 注意选择社区版…

AWS-亚马逊网络服务(基础服务)-AWS 定价计算器-概述与动手部署:

让我们来概述并亲身实践如何使用 AWS 定价计算器来计算 概述&#xff1a; AWS 定价计算器是 Amazon Web Services (AWS) 提供的基于 Web 的工具&#xff0c;可帮助用户估算其特定用例的 AWS 服务成本。欢迎来到雲闪世界。 它允许客户建模他们的基础设施并根据他们打算使用的…

【AI 绘画】更快?更省显存?支持 FLUX?使用绘世启动器安装 SD WebUI Forge

使用绘世启动器安装 SD WebUI Forge 下载绘世启动器 绘世启动器下载地址1&#xff1a;https://gitee.com/licyk/term-sd/releases/download/archive/hanamizuki.exe 绘世启动器下载地址2&#xff1a;https://www.bilibili.com/video/BV1ne4y1V7QU 新建一个文件夹取名sd-webui-…

中仕公考怎么样?公务员考试什么时候补录?

公务员考试补录的时间和方法通常因地区和职位的不同有所区别&#xff0c;一般来说&#xff0c;这一过程会在面试、体检和考核环节完成后启动。 如果在招录过程中出现职位空缺或者并未全部招满的情况&#xff0c;就会进行补录。用人单位会通过其官方或公告形式公布相关信息&…

指针5.回调函数与qsort

今天来学习回调函数与qsort 目录 1.回调函数实现模拟计算器代码的简化原代码运行结果简化代码运行结果 qsort函数排序整型数据代码运行结果 qsort排序结构数据代码 qsort函数的模拟实现代码运行结果 总结 1.回调函数 回调函数就是⼀个通过函数指针调用的函数。 如果你把函数的…

JavaEE第22节 TCP段(报文)结构剖析

目录&#xff08;关于字段有不理解的&#xff0c;哪里不会点哪里&#x1f618;&#xff09; 逻辑结构字段解析一、源端口&目的端口二、序号&确认序号三、头部长度四、保留位五、特殊标志位六、窗口大小七、校验和八、紧急指针九、可选选项十、数据 逻辑结构 如图&…

入门Java编程的知识点—>Http协议(day20)

了解http协议是什么掌握http请求信息、响应信息格式 项目目标&#xff1a; 实现本地客户端与服务器一问一答的请求与响应,了解http协议即可. 项目步骤&#xff1a; 服务器端代码编写 先在当前src文件下新建一个包: webserver,再该包下创建一个类Server&#xff0c;书写代码如…

带着耐心细心平常心和编程共舞

编程是什么&#xff1f;一个工具、一门技术还是一个爱好&#xff0c;不同的对待方法会带来不同的心态、产生不同的结果。编程需要扎实的基础、严密的思维和开阔的视角&#xff0c;新技术和框架日新月异&#xff0c;只有抱着科学、乐在其中的态度才能掌握高效的学习、实践方法。…

Matrix:重塑APM领域,以简驭繁的性能监控新纪元

在数字化转型的浪潮中&#xff0c;应用程序的性能监控&#xff08;APM&#xff09;已成为企业IT架构中不可或缺的一环。随着业务复杂度的提升和用户对体验要求的日益增高&#xff0c;如何高效、精准地监控并优化应用性能&#xff0c;成为了每个开发者和技术团队面临的重大挑战。…

机器学习(五) -- 监督学习(8) --神经网络1

系列文章目录及链接 上篇&#xff1a;机器学习&#xff08;五&#xff09; -- 无监督学习&#xff08;2&#xff09; --降维2 下篇&#xff1a; 前言 tips&#xff1a;标题前有“***”的内容为补充内容&#xff0c;是给好奇心重的宝宝看的&#xff0c;可自行跳过。文章内容被…

2.3导数与微分的基础与应用

1. 导数的基本概念 大家好&#xff0c;欢迎来到我们的数学大讲堂&#xff01;今天我们要聊聊一个有点酷又有点恐怖的东西——导数。别担心&#xff0c;不是让你在黑板上画曲线的那种&#xff0c;而是关于“变化率”的一种数学表达。 那么&#xff0c;什么是导数呢&#xff1f…

利用实用规模量子计算模拟宇宙中最极端的环境

华盛顿大学和劳伦斯伯克利国家实验室最近的研究展示了可扩展的技术&#xff0c;有朝一日可以实现最高能量下的基础物理实验模拟。 目录 核物理和高能物理的实用规模模拟 我们的模拟方法 Qiskit 如何使我们的实验成为可能 展望量子模拟技术的未来 粒子物理学的标准模型囊括了我们…

005.Python爬虫系列_浏览器开发者工具(详解)

我 的 个 人 主 页:👉👉 失心疯的个人主页 👈👈 入 门 教 程 推 荐 :👉👉 Python零基础入门教程合集 👈👈 虚 拟 环 境 搭 建 :👉👉 Python项目虚拟环境(超详细讲解) 👈👈 PyQt5 系 列 教 程:👉👉 Python GUI(PyQt5)文章合集 👈👈 Oracle数…

web渗透:XXE漏洞

XXE漏洞的原理 XXE&#xff08;XML External Entity Injection&#xff09;漏洞是由于应用程序在解析XML输入时&#xff0c;没有正确处理外部实体&#xff08;External Entities&#xff09;的引用而产生的安全漏洞。XML外部实体可以引用本地或远程的内容&#xff0c;攻击者可…