三层时间轮的实现

时间轮

时间轮就像时钟表盘一样,以时钟表盘为例,时钟表盘有秒针分针时针,60秒=1分钟,60分钟=1小时。秒就是最小时间单位。

我们会根据任务想要定时的时间先把任务挂载到秒、分、时的层级上。然后每到下一秒就执行这一秒下面挂载的任务,如果进入下一分钟,就把这一分钟的时间都映射到秒层级。这就是简单的秒分时时间轮。

时间轮有时间指针,每隔最小单位的时间,时间指针就移动一格。

所以时间轮就像循环数组一样,但C语言没有循环数组,所以就使用普通数据+取余运算实现循环数据的效果。

对于同一时间任务采用尾插法将任务连接起来。因为尾插法可以实现先插进来的先执行,按照先进先执行的规则执行。

对于大于60s的任务放在分针层级,大于60分的任务放在小时层级。

当0分钟59秒 到 1分钟时,就会把这一分钟下的任务重新映射到秒针层级。

这个映射是怎么操作的?

比如一个70s的任务在插入时会插入到1分钟对应的链表下,到1分钟后,那这个70s的任务取出来,再进行添加,那岂不是还是原来的位置?难道添加的函数不一样?第一次添加时会进行区分秒分时层级,再取出来重新添加时就调用下一层级的添加函数?

答:插入函数时分级的。

// < 60秒 → 秒轮,60~3600秒 → 分轮,> 3600秒 → 时轮

时间轮结构体有一个参数是uint32_t time; 用来记录当前时间轮的时间,程序运行时为0,每隔1s就加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 msec = time - current_time; // 距离任务到期还剩多少时间
    // 根据剩余时间进行分级插入
    if (msec < ONE_MINUTE) {
        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);
    }
}

在插入时,会计算

uint32_t msec = time - current_time;

这个msec 参数就是程序运行到这里的时间 距离 任务到期 还有多长时间。

比如在最开始插入70s的定时任务,那么msec =70 - 0 = 70,就会插入到分轮。

当从分轮重新映射到秒轮时,会重新计算msec 。当重新映射时,肯定时到1分钟了,这是msec = 70-60 = 10。这样就映射到10s的位置。

时间轮适用于多线程,通常有一个线程专门负责时间轮的处理,只进行移动时间指针等有关时间轮的操作,但具体任务的执行交给其他专门处理的工作线程。其中时间轮线程会通过消息的形式通知工作线程去执行任务。时间轮只检测不执行。

多线程一般使用时间轮。单线程使用红黑树或者最小堆,跳表使用的较少。而且最小堆分为最小二叉堆 和 最小四叉堆,最小四叉堆相比二叉堆有5%的性能提升。

clock_gettime

clock_gettime是基于Linux C语言的时间函数,可以用于计算精度和纳秒。

复制代码
//头文件
#include <time.h>

//函数原型
int clock_gettime( clockid_t clock_id,struct timespec * tp );

// timespec 结构体
struct timespec { 
    __time_t tv_sec; /* 秒 */ 
    __syscall_s long_t tv_nsec; /* 纳秒 */
};

clock_id: 是指使用时钟的类型

复制代码
CLOCK_REALTIME:是指系统时间,随着系统时间的改变而改变。系统时钟会随用户而改变。系统当前的日历时间。1970年1月1日 00:00:00 UTC 开始计时。

CLOCK_MONOTONIC:指从系统启动时开始计时。不受系统影响,也不会被用户改变。

CLOCK_PROCESS_CPUTIME_ID:指这个进程运行到当前代码时,系统花费的时间。

CLOCK_THREAD_CPUTIME_ID:指这个线程运行到当前代码时,系统花费的时间。

#include<stdio.h>
#include<time.h>

int main(){
    struct timespec now;

    clock_gettime(CLOCK_MONOTONIC,&now);// 系统开机到运行程序的时间
    printf("CLOCK_MONOTONIC: Seconds = %ld \t Nanoseconds = %ld\n", now.tv_sec, now.tv_nsec);

    clock_gettime(CLOCK_REALTIME,&now);// 从1970年1月1日 00:00:00计时的时间
    printf("CLOCK_REALTIME: Seconds = %ld \t Nanoseconds = %ld\n", now.tv_sec, now.tv_nsec);

    clock_gettime(CLOCK_THREAD_CPUTIME_ID,&now);// 线程运行的时间
    printf("CLOCK_THREAD_CPUTIME_ID: Seconds = %ld \t Nanoseconds = %ld\n", now.tv_sec, now.tv_nsec);

    clock_gettime(CLOCK_PROCESS_CPUTIME_ID,&now);// 进程运行的时间
    printf("CLOCK_PROCESS_CPUTIME_ID: Seconds = %ld \t Nanoseconds = %ld\n", now.tv_sec, now.tv_nsec);
    return 0;
}

wait-24@DESKTOP-NFQTH04:~/code/time$ ./time 
CLOCK_MONOTONIC: Seconds = 641   Nanoseconds = 154814100
CLOCK_REALTIME: Seconds = 1777381108     Nanoseconds = 813390800
CLOCK_THREAD_CPUTIME_ID: Seconds = 0     Nanoseconds = 529700
CLOCK_PROCESS_CPUTIME_ID: Seconds = 0    Nanoseconds = 531300

代码执行

接口

暴露给用户的接口。

复制代码
void init_timer(void); // 初始化定时器系统
timer_node_t* add_timer(int time, handler_pt func); // 添加定时器,time为相对时间(秒),func为回调函数
void del_timer(timer_node_t *node); // 删除定时器(软删除,标记为已取消)
void check_timer(int *stop); // 主驱动循环,阻塞
void clear_timer(); // 释放所有资源
time_t now_time(); // 获取时钟

用户只需要,初始化,然后添加定时器任务,并在最后记得释放资源即可。

初始化时间轮

创建一个定时器结构体并初始化为空,然后获取一下系统时间作为当前时间。

复制代码
void
init_timer(void) {
    TI = create_timer();
    TI->current_point = now_time(); // 获取初始化时的系统时钟时间
}

添加定时器任务

添加定时时间t的任务时,是以程序运行到添加定时器代码时开始计时t,而不是从程序一开始运行就直接开始计时t。

复制代码
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, expire_time at %lu\n", TI->time, node->expire, now_time(), node->expire + now_time());
    node->callback = func;
    node->cancel = 0;
    // 若 time <= 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;
}

怎么准确知道系统到了下一秒?

每0.2s获取一次系统,如果获取的时间与记录的时间不一样,意味着已经度过这一秒,所以就要推进一秒。

复制代码
// 主驱动循环:每 200ms 轮询一次,追赶流逝的秒数并触发定时器
// 自上轮检查以来每流逝一秒,调用一次 timer_update()
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); // 0.2s
    }
}

// 单调时钟(秒)------不受系统时间跳变影响
time_t
now_time() {
    struct timespec ti;
    clock_gettime(CLOCK_MONOTONIC, &ti); // 获取单调递增的系统时钟(不受NTP调整影响)从开机计算
    return ti.tv_sec;// ti.tv_sec:秒  ti.tv_nsec:纳秒
}

删除定时器

采用软删除,将删除标志置一。

会在执行任务的时候,进行检测,如果删除标记置一了,就会释放这个任务。

复制代码
// 软删除:标记为已取消,节点在其所属槽过期时释放
void
del_timer(timer_node_t *node) {
    node->cancel = 1;
}

执行任务

每0.2s检测一下时间,如果进入下一秒就推进时钟,然后使用timer_update函数执行对应槽的任务。

复制代码
// 主驱动循环:每 200ms 轮询一次,追赶流逝的秒数并触发定时器
// 自上轮检查以来每流逝一秒,调用一次 timer_update()
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); // 0.2s
    }
}

释放资源

将所有malloc申请的空间全部释放。

复制代码
// 释放三轮中所有残留的定时器节点(退出前清理)
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]);
    }
}

内部实现

结构体

一个单个任务结构体,一个时间轮结构体。

单个任务结构体

复制代码
struct timer_node {
    struct timer_node *next;  // 链表指针
    uint32_t expire;          // 到期时间(秒)
    handler_pt callback;      // 回调函数
    uint8_t cancel;           // 删除标记 0:未取消 1:已取消
};

时间轮结构体

复制代码
// 3层时间轮:随时间推进,定时器从时轮 → 分轮 → 秒轮逐级下放
typedef struct timer {
    link_list_t second[SECONDS]; // 秒轮(每槽 1 秒)
    link_list_t minute[MINUTES]; // 分轮(每槽 1 分)
    link_list_t hour[HOURS];    // 时轮(每槽 1 小时)
    spinlock_t lock;            // 用于多线程安全操作时间轮
    uint32_t time;              // 当前时间轮时间(秒,范围 0 ~ HALF_DAY-1)程序启动时为0,每过一秒+1
    time_t current_point;       // 上次观察到的单调时钟秒数
}timer_st;

// 单向链表,带头哨兵节点和尾指针,支持 O(1) 追加
typedef struct link_list {
    timer_node_t head; // 哨兵节点(非真实定时器)
    timer_node_t *tail;
}link_list_t;

尾插法

插入结点使用尾插法。

复制代码
// O(1) 追加到链表尾部,尾插法
static void
link_to(link_list_t *list, timer_node_t *node) {
    list->tail->next = node;
    list->tail = node;
    node->next=0;
}

分级添加结点

添加结点,分级添加

< 60秒 → 秒轮,60~3600秒 → 分轮,> 3600秒 → 时轮

复制代码
// 根据剩余时间将定时器分级插入对应的轮层:
//   < 60秒 → 秒轮,60~3600秒 → 分轮,> 3600秒 → 时轮
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; // 距离任务到期还剩多少时间
    // 根据剩余时间进行分级插入
    if (msec < ONE_MINUTE) {
        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);
    }
}

剩余时间 msec = expire - current_time
├─ msec < 60秒    → 插入 second[expire % 60]
├─ msec < 3600秒  → 插入 minute[(expire/60) % 60]
└─ msec >= 3600秒 → 插入 hour[(expire/3600) % 12]

根据定时时间的不同插入到不同级别的时间轮。

时钟推进与重新映射

时钟以每秒推进一次,对于此定时器秒就是最小基本单位。

时钟前进,到整分或者整时,就会重新映射到下一级时间轮。

重新映射,即取出任务,并重新添加。

在插入时,会计算

uint32_t msec = time - current_time;

这个msec 参数就是程序运行到这里的时间 距离 任务到期 还有多长时间。

比如在最开始插入70s的定时任务,那么msec =70 - 0 = 70,就会插入到分轮。

当从分轮重新映射到秒轮时,会重新计算msec 。当重新映射时,肯定时到1分钟了,这是msec = 70-60 = 10。这样就映射到10s的位置。

当3540s时 分轮idx = (3540/60)%60 = 59,重新映射分轮第59槽的定时器。

当3600s时,分轮idx = (3600/60)%60 =0,分轮不会出发映射。时轮idx=(3600/3600)%12=1,重新映射时轮第1槽的定时器。

当3660s时,分轮idx = (3660/60)%60 =1,重新映射分轮第1槽的定时器。直接return,时轮不会触发。

在插入的时候3600也是存放在时轮第1槽。时轮第0槽存放的是什么?分轮第0槽存放的是什么?

0-59s在秒轮,

60-119在分轮第1槽,

120-179在分轮第2槽,

......

3540-3599在分轮第59槽。

3600-7199在时轮第1槽,

7200-10799在时轮第2槽,

......

3600*11-3600*12-1在时轮第11槽。

分轮第0槽永远不会有定时器。

时轮第0槽会在12小时回绕时,放入定时器。

复制代码
// 将高层槽level第idx槽中的定时器逐级下放到低层轮(时→分→秒)
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;
    }
}

// 时钟指针前进一秒,高层轮定时器逐级下放
// 每到整秒:重映射当前分钟槽
// 每到整分:重映射当前小时槽
// 到达半日周期(回绕):重映射 hour[0] 处理 12 小时回绕
static void
timer_shift(timer_st *T) {
    uint32_t ct = ++T->time % HALF_DAY;
    // ++T->time 先自增,后返回自增后的值
    // T->time++ 先返回原值,后自增
    if (ct == 0) { // ct == 0 表示刚好走完一个 12 小时周期
        remap(T, T->hour, 0); // 将时轮的第 0 槽(hour[0])中的所有定时器重新映射 
    } else {
        if (ct % SECONDS == 0) { // 每到整分钟时,处理当前小时槽(跳过 hour[0])
            // 两个括号 两个作用域 两个作用域中的 idx 是不同的变量,互不影响
            {
                uint32_t idx = (uint32_t)(ct / ONE_MINUTE) % MINUTES;
                if (idx != 0) { // 同样分轮跳过 0 号槽
                    remap(T, T->minute, idx);// 将分轮的第 idx 槽中的所有定时器重新映射 
                    return; // 这里的return可以保障一次只迁移一个槽,当时轮和分轮同时满足时,分轮优先
                }
            }// ← idx 变量在这里销毁
            {
                uint32_t idx = (uint32_t)(ct / ONE_HOUR) % HOURS;
                if (idx != 0) { // 同样时轮跳过 0 号槽
                    remap(T, T->hour, idx);// 将时轮的第 idx 槽(hour[idx])中的所有定时器重新映射 
                }
            }
        }
    }
}

补充解释

每秒的执行都会将当前秒槽下的任务执行并全部free掉。那怎么实现循环定时呢?每次都重新添加?

是的,要重新添加。还可以直接不释放此任务,然后根据定时时间继续添加到对应的时间槽中。

为什么timer_update会有两次执行?

每次 timer_update 调用会跨越两个秒槽。

这里的两次执行是在59秒进入timer_update函数,执行59秒的任务,然后推进1s,执行60秒的任务。

60秒再次进入timer_update函数,因为在59秒进入timer_update函数时执行了60秒的任务,执行完后就被释放了。所以这里的60秒进入timer_update函数,只会判断一下,发现已经执行完了,就推进一秒,再执行61秒的任务。

其实就是在59s进入timer_update函数执行60s的任务(59s的任务已经在58s进入timer_update函数执行完了),在60s进入timer_update函数执行61s的任务。

定时器在到期时刻的第一时间就被执行,而不是等到该秒结束。可以做到低延迟。

做一次测试,添加58s,59s,60s,61s任务,使用两次执行,运行程序。使用一次执行,运行程序。看一下结果。

如果只有一次执行,都整体延迟了一秒

有两次执行,完美执行

完整代码

clock-timer.c

c 复制代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "spinlock.h"
#include "clock-timer.h"
 
// 3层时间轮:秒轮(60槽)、分轮(60槽)、时轮(12槽)
// 覆盖12小时周期(43200秒),到期后回绕
#define SECONDS 60
#define MINUTES 60
#define HOURS   12
#define ONE_HOUR 3600
#define ONE_MINUTE 60
#define HALF_DAY 43200 // 最大定时时间 12*3600 12小时
 
// 单向链表,带头哨兵节点和尾指针,支持 O(1) 追加
typedef struct link_list {
    timer_node_t head; // 哨兵节点(非真实定时器)
    timer_node_t *tail;
}link_list_t;

// 3层时间轮:随时间推进,定时器从时轮 → 分轮 → 秒轮逐级下放
typedef struct timer {
    link_list_t second[SECONDS]; // 秒轮(每槽 1 秒)
    link_list_t minute[MINUTES]; // 分轮(每槽 1 分)
    link_list_t hour[HOURS];    // 时轮(每槽 1 小时)
    spinlock_t lock;            // 用于多线程安全操作时间轮
    uint32_t time;              // 当前时间轮时间(秒,范围 0 ~ HALF_DAY-1)程序启动时为0,每过一秒+1
    time_t current_point;       // 上次观察到的单调时钟秒数
}timer_st;

// 全局定时器实例(单例)
static timer_st * TI = NULL;

// 将槽中所有节点摘除,重置链表为空,返回第一个节点
// 用于上一级的时间轮映射到下一级时间轮中
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;
}

// O(1) 追加到链表尾部,尾插法
static void
link_to(link_list_t *list, timer_node_t *node) {
    list->tail->next = node;
    list->tail = node;
    node->next=0;
}

// 根据剩余时间将定时器分级插入对应的轮层:
//   < 60秒 → 秒轮,60~3600秒 → 分轮,> 3600秒 → 时轮
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; // 距离任务到期还剩多少时间
    // 根据剩余时间进行分级插入
    if (msec < ONE_MINUTE) {
        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);
    }
}

// 将高层槽level第idx槽中的定时器逐级下放到低层轮(时→分→秒)
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;
    }
}

// 时钟指针前进一秒,高层轮定时器逐级下放
// 每到整秒:重映射当前分钟槽
// 每到整分:重映射当前小时槽
// 到达半日周期(回绕):重映射 hour[0] 处理 12 小时回绕
static void
timer_shift(timer_st *T) {
    uint32_t ct = ++T->time % HALF_DAY;
    // ++T->time 先自增,后返回自增后的值
    // T->time++ 先返回原值,后自增
    if (ct == 0) { // ct == 0 表示刚好走完一个 12 小时周期
        remap(T, T->hour, 0); // 将时轮的第 0 槽(hour[0])中的所有定时器重新映射 
    } else {
        if (ct % SECONDS == 0) { // 每到整分钟时,处理当前小时槽(跳过 hour[0])
            // 两个括号 两个作用域 两个作用域中的 idx 是不同的变量,互不影响
            {
                uint32_t idx = (uint32_t)(ct / ONE_MINUTE) % MINUTES;
                if (idx != 0) { // 同样分轮跳过 0 号槽
                    remap(T, T->minute, idx);// 将分轮的第 idx 槽中的所有定时器重新映射 
                    return; // 这里的return可以保障一次只迁移一个槽,当时轮和分轮同时满足时,分轮优先
                }
            }// ← idx 变量在这里销毁
            {
                uint32_t idx = (uint32_t)(ct / ONE_HOUR) % HOURS;
                if (idx != 0) { // 同样时轮跳过 0 号槽
                    remap(T, T->hour, idx);// 将时轮的第 idx 槽(hour[idx])中的所有定时器重新映射 
                }
            }
        }
    }
}

// 链表中未取消的定时器任务会触发回调函数,然后free;已经取消的定时器任务,会直接free
// 在调用dispatch_list时,必须确保current不为空,使用do-while可以省去一次空判断
// 若使用while会多一次空判断
static void
dispatch_list(timer_node_t *current) {
    do {
        timer_node_t * temp = current;
        current=current->next;
        if (temp->cancel == 0) //0:未取消 1:已取消
            temp->callback(temp); // 如果未取消,就执行回调函数
        free(temp);
    } while (current);
}

// 执行当前秒槽中所有到期的定时器
// 每秒调用一次
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]); // 取出当前秒槽中的所有定时器节点
        spinlock_unlock(&T->lock);
        dispatch_list(current);
        spinlock_lock(&T->lock);
    }
} 

// 一次滴答:执行当前槽 → 推进时钟 → 再次执行
// 两次执行是为了处理 shift 后定时器级联落入当前槽的边界情况
static void
timer_update(timer_st *T) {
    spinlock_lock(&T->lock);
    timer_execute(T);   // 第一次执行(当前秒槽)
    timer_shift(T);     // 时间指针前进1秒+级联迁移
    timer_execute(T);   // 第二次执行(新秒槽)
    spinlock_unlock(&T->lock);
}

// 分配并零初始化时间轮,将所有链表槽初始化为空
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;
}

void
init_timer(void) {
    TI = create_timer();
    TI->current_point = now_time(); // 获取初始化时的系统时钟时间
}

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, expire_time at %lu\n", TI->time, node->expire, now_time(), node->expire + now_time());
    node->callback = func;
    node->cancel = 0;
    // 若 time <= 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;
}

// 软删除:标记为已取消,节点在其所属槽过期时释放
void
del_timer(timer_node_t *node) {
    node->cancel = 1;
}

// 主驱动循环:每 200ms 轮询一次,追赶流逝的秒数并触发定时器
// 自上轮检查以来每流逝一秒,调用一次 timer_update()
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); // 0.2s
    }
}

// 释放三轮中所有残留的定时器节点(退出前清理)
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]);
    }
}

// 单调时钟(秒)------不受系统时间跳变影响
time_t
now_time() {
    struct timespec ti;
    clock_gettime(CLOCK_MONOTONIC, &ti); // 获取单调递增的系统时钟(不受NTP调整影响)从开机计算
    return ti.tv_sec;// ti.tv_sec:秒  ti.tv_nsec:纳秒
}

clock-timer.h

c 复制代码
#ifndef _MARK_TIMEWHEEL_
#define _MARK_TIMEWHEEL_
#include <time.h>
#include <stdint.h>
#include <stddef.h>

typedef struct timer_node timer_node_t;
typedef void (*handler_pt) (struct timer_node *node); // 回调函数类型

// 定义定时器节点结构体
struct timer_node {
    struct timer_node *next;  // 链表指针
    uint32_t expire;          // 到期时间(秒)
    handler_pt callback;      // 回调函数
    uint8_t cancel;           // 删除标记 0:未取消 1:已取消
};

void init_timer(void); // 初始化定时器系统
timer_node_t* add_timer(int time, handler_pt func); // 添加定时器,time为相对时间(秒),func为回调函数
void del_timer(timer_node_t *node); // 删除定时器(软删除,标记为已取消)
void check_timer(int *stop); // 主驱动循环,阻塞
void clear_timer(); // 释放所有资源
time_t now_time(); // 获取时钟

#endif

clock-main.c

c 复制代码
#include <stdio.h>
#include "clock-timer.h"

void do_timer(timer_node_t *node) {
    (void)node;
    printf("do_timer expired now_time:%lu\n", now_time());
}

int stop = 0;
int main() {
    init_timer();
    add_timer(3, do_timer);
    add_timer(8, do_timer);
    add_timer(9, do_timer);
    add_timer(10, do_timer);
    add_timer(11, do_timer);
    add_timer(58, do_timer);
    add_timer(59, do_timer);
    add_timer(60, do_timer);
    add_timer(61, do_timer);
    add_timer(62, do_timer);

    check_timer(&stop);
    clear_timer();
    return 0;
}

spinlock.h

c 复制代码
#ifndef SPINLOCK_H
#define SPINLOCK_H

typedef struct spinlock {
	int lock;
}spinlock_t;

void spinlock_init(spinlock_t *lock) {
	lock->lock = 0;
}

void spinlock_lock(spinlock_t *lock) {
	while (__sync_lock_test_and_set(&lock->lock, 1)) {}
}

int spinlock_trylock(spinlock_t *lock) {
	return __sync_lock_test_and_set(&lock->lock, 1) == 0;
}

void spinlock_unlock(spinlock_t *lock) {
	__sync_lock_release(&lock->lock);
}

void spinlock_destroy(spinlock_t *lock) {
	(void) lock;
}

#endif

运行命令

gcc clock-timer.c clock-main.c -o clock -I./ -lpthread

相关推荐
科技牛牛2 小时前
街道级IP定位能解决什么问题?哪些团队更适合用
网络·网络协议·tcp/ip·街道级ip定位
信徒_2 小时前
WAF 安全防护
网络·安全
luojiezong2 小时前
锐捷一机一网3.0全新发布:打造一张可成长的网络
网络
日取其半万世不竭2 小时前
用 Netdata 实时监控服务器,比 Prometheus + Grafana 轻量得多
linux·服务器·网络·系统架构·负载均衡·zabbix·grafana
咸鱼永不翻身3 小时前
Lua脚本事件检查工具
unity·lua·工具
2401_881828323 小时前
交换综合实验报告
网络
d111111111d3 小时前
了解Modbus
网络·笔记·stm32·单片机·嵌入式硬件·学习
Hui_AI7203 小时前
基于RAG的农产品GEO溯源智能问答系统实现
开发语言·网络·人工智能·python·算法·创业创新
程序员JerrySUN3 小时前
Jetson边缘嵌入式实战课程第二讲:JetPack 和 SDK Manager 是什么
c语言·开发语言·网络·udp·音视频