时间轮定时器实现

1 定时器应用场景

a. 批量处理定时任务
b. 心跳检测
c. 和网络事件一起使用

2 时间轮

2.1 什么是时间轮

  • 时间是一种调度模型, 为高效解决调度任务而产生的

2.2 为什么要用时间轮

在讨论为什么定时器要用时间轮之前, 我们先了解一下几种其他实现方式的调度器

a. 有序队列

  • 添加/删除任务: 遍历每一个节点, 找到相应的位置插入, 因此时间复杂度为O(n)
  • 处理到期任务: 取出最小定时任务为首节点, 因此时间复杂度为O(1)

b. 红黑树

有序队列的性能瓶颈在于插入任务和删除任务(查找排序), 而树形结构能对其进行优化, 这里可以以红黑树为例子

  • 添加/删除/查找任务: 红黑树能将排序的的时间复杂度降到O(log2N)
  • 处理到期任务: 红黑树查找到最后过期的任务节点为最左侧节点, 因此时间复杂度为O(log2N)

c. 最小堆

  • 添加/查找任务: 时间复杂度为O(log2N)
  • 删除任务: 时间复杂度为O(n), 可通过辅助数据结构(map)来加快删除操作
  • 处理到期任务: 最小节点为根节点, 时间复杂度为O(1)

d. 跳表

  • 添加/删除/查找任务: 时间复杂度为O(log2N)
  • 处理到期任务: 最小节点为最左侧节点, 时间复杂度为O(1), 但空间复杂度比较高, 为O(1.5n)

不难看出上面的方法在添加/删除定时任务时都要话比较长的时间, 特别是定时任务越多, 所花费的时间就越多, 因此就需要一种方法能够减少添加/删除任务时所花费的时间, 而这就能借助时间轮来实现了

** 题外话: 具体选择用哪种数据结构来实现定时器还是得根据实际的使用场景, 这里就不对这几种数据结构进行详细分析 **

2.3 时间轮的原理

就像钟表那样:

  • 低层级(秒针)
  • 高层级(分针)
  • 低层级每次时间+1就移动一格(最小精度), 低层级如果移动一圈后, 高层级就移动一格

2.4 时间轮的实现

在实现完整的时间轮(多层级)之前我们需要理解单层级时间轮的实现

2.41 单层级时间轮

单层级时间轮指的是像秒表那样, 只有秒针, 没有分针和时针, 因此只能定时很小范围内的定时任务(0~60s), 但理解它对之后引申到多层级时间轮有很大帮助

1)首先我们来讨论如何用数据结构来抽象秒针的运转

int seconds[60]; // 数组来表示表盘刻度
++tick % 60;    // 每秒钟移动一格(++tick); 对tick取余则使秒针能一直落在[0, 59]的区间上

2)那么如何添加定时任务呢?

添加定时任务就是在当前的时间节点加上定时时间, 然后再%60映射到seconds[60]数组相应的位置上即可

拓展一下:

  • 也许你会想到, 如果在同一节点下有多个定时任务该怎么办

这里我们就可以修改下int seconds[60], 将int变为一个链表的结构体即可, 那么我们每次添加新的任务就可以用链表链起来;
即如果改时间节点下有其他任务, 就将新添加的任务加到链表尾部;

typedef struct timer_node {
    struct timer_node *next;
} timer_node_t;

typedef struct link_list {
    timer_node_t head;
    timer_node_t *tail;
} link_list_t;

typedef struct timer {
    link_list_t seconds[60];
} s_timer_t;

该图为上面结构体示意图
在这里插入图片描述

3)执行任务:

当移到到相应的区间上时, 就将该区间所以的任务节点取出来;

4)取消任务:

  • 因为我们添加任务时并没有记录节点添加的位置, 无法找到任务节点是在哪里, 所以我们可以在任务节点的结构体上添加一个取消任务的字段;

在执行任务时, 就会检查这个字段的真假, 如果为真便是取消了的任务;
那么遇到取消了的任务时, 就可以不去执行相应的定时任务即可

typedef struct timer_node {
    struct timer_node *next;
    uint8_t cancel;             // 该字段用来判断任务是否被取消
} timer_node_t;
2.42 多层级时间轮

单层级时间轮只能定时[0, 59]区间的时间, 而超过这个时间的话是无法解决的, 所以在实际环境中并不适用, 这时候我们可以在它的基础上拓展为多层级时间轮

  • 多层级时间轮是在单层级时间轮的基础上加多几层, 如图所示:

注: 该图单层级时间轮的定时区间我们现在设为[0, 255]
在这里插入图片描述

1)多层级时间轮的原理

a.第一层和上面单层级的一样, 第二层当第一层tick == 256时, 第二层指针+1; 第n层为上一层指针到达数组大小时, 指针+1
b.每次当指针指到相应的数组格子时, 就将该格子的链表取出, 并重新映射到上一层的数组上
c.当第一层指针指到相应的数组格子时, 则该格子下的链表任务到期(即单层级情况)

2)如何确定定时的时间是加在哪个层级上的哪个数组位置?

  • 多层级第一层: [0, 256), (定时时间 - 第一层最大时间)%该层格子数, 第一层最大时间: 第一层数组大小
  • 多层级第二层: [256, 256 * 64), (定时时间 - 前两层最大时间)%该层格子数, 前两层最大时间: 第一层数组大小 + (第一层数组大小 * 第二层数组大小)
  • 多层级第n层: 同上以此类推

2.5 代码实现

https://github.com/SPWT/server_learn/tree/main/7_1_timer

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Netty是一个基于Java的高性能网络通信框架,它提供了一些方便的功能,包括时间定时任务。时间是一种用于执行定时任务的数据结构,它可以提高定时任务的触发精度和执行效率。 在Netty中,时间定时任务是通过`HashedWheelTimer`类实现的。下面是一个简单的示例代码,演示如何在Netty中使用时间定时任务: ```java import io.netty.util.HashedWheelTimer; import io.netty.util.Timeout; import io.netty.util.TimerTask; public class TimeWheelExample { public static void main(String[] args) { // 创建时间定时器 HashedWheelTimer timer = new HashedWheelTimer(); // 创建定时任务 TimerTask task = new TimerTask() { @Override public void run(Timeout timeout) throws Exception { System.out.println("定时任务执行"); } }; // 将定时任务提交给时间定时器,延迟2秒后执行 timer.newTimeout(task, 2, TimeUnit.SECONDS); } } ``` 在上面的示例中,我们首先创建了一个`HashedWheelTimer`实例,然后创建了一个`TimerTask`对象,定义了要执行的定时任务。最后,我们使用`timer.newTimeout()`方法将定时任务提交给时间定时器,并指定了延迟时间为2秒。 当时间定时器触发定时任务时,会调用`run()`方法执行任务。在这个例子中,定时任务执行时,会简单地打印一条消息。 需要注意的是,时间定时任务仅限于在Netty中使用,如果你想在其他环境或框架中使用时间定时任务,可能需要使用其他的定时任务实现方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值