首页 > 编程语言 >TimerWheel(计时轮)在Rust中的实现及源码解析

TimerWheel(计时轮)在Rust中的实现及源码解析

时间:2024-06-12 08:59:46浏览次数:20  
标签:mut self timer 60 源码 usize TimerWheel id Rust

计时器轮(TimerWheel),模拟时钟格式组成的高效计时器

TimerWheel算法原理

  1. 环形数据结构:TimerWheel,即时间轮,是一个环形的数据结构,类似于时钟的面,被等分为多个格子或槽位(slot)。

  2. 槽位时间间隔:每个槽位代表一个固定的时间间隔,例如1毫秒、1秒等。这个时间间隔决定了定时器的精度。

  3. 初始化:在算法开始时,需要初始化时间轮,包括设定时间轮的大小(即槽位的数量)和每个槽位代表的时间间隔。即当插入数据后即不允许修改时轮信息。

TimerWheel算法的优缺点

  • 优点
    • 高效性:通过利用循环数组和指针的特性,时间轮算法可以以固定的时间复杂度(通常是O(1))来处理任务的调度和触发,这提供了一种高效的定时任务管理方式。
    • 可扩展性:如果单个时间轮无法满足需求,可以通过增加时间轮的层数来扩展其表示的时间范围。多层时间轮可以处理更长时间跨度的定时任务。
  • 缺点
    • 精度越高资源耗费多:时间轮的精度取决于每个槽位代表的时间间隔。通过调整这个时间间隔,可以在精度和资源消耗之间找到平衡。
    • 删除效率:通常时轮未索引key值存储位置,删除需要遍历整个列表,复杂度为O(n)

结构设计

在TimerWheel的结构中,我们需要有两层结构,一层TimerWheel管理整个类,用OneTimerWheel来管理每个轮,此外定义了trait为Timer,类型必须实现该接口以管理类的递进时长。

管理类

管理类里定义了时轮的最大最小及其它的信息

pub struct TimerWheel<T:Timer> {
    /// 时轮的最大轮,以时钟为例就是时针
    greatest: *mut OneTimerWheel<T>,
    /// 时轮的最小轮,以时钟为例就是秒针
    lessest: *mut OneTimerWheel<T>,
    /// 时轮的最小间隔,以时间为例就是秒
    min_step: usize,
    /// 维护定时器id
    next_timer_id: usize,
    /// 离的最近的id
    delay_id: usize,
    /// 总共的递进步长,缓存优化触发
    all_deltatime: usize,
    /// 当时时轮里的元素个数
    len: usize,
}

单轮管理

当前轮管理的当前指针及每个糟里的元素

pub struct OneTimerWheel<T:Timer> {
    /// 当时指针指向的位置,如秒针指向3点钟方向
    index: usize,
    /// 当前结构的容量,如表示秒的为60的容量
    capation: usize,
    /// 当前结构步长,如分钟就表示60s的
    step: usize,
    /// 当前槽位容纳的元素
    slots: Vec<Vec<Entry<T>>>,
    /// 当前轮结构的父轮,如当前是分的,那父轮为时轮
    parent: *mut OneTimerWheel<T>,
    /// 当前轮结构的子轮,如当前是分的,那父轮为秒轮
    child: *mut OneTimerWheel<T>,
    /// 当前轮的名字,辅助定位
    name: &'static str,
}

接口Timer

类必须时间Timer结构,以统一知道他对当前时间的间隔。必须覆写when以确定当前的offset。

pub trait Timer {
    /// 当时与现在的间隔,以确定插入确定的槽
    fn when(&self) -> usize;
    /// 可能需要修改对象,此处用可变值
    fn when_mut(&mut self) -> usize {
        self.when()
    }
}

源码分析

初始化

因为时轮是后续设置,最小刻度由最小数额确定。故初始化不需要任何参数:

impl<T:Timer> TimerWheel<T> {
    pub fn new() -> Self {
        Self {
            greatest: ptr::null_mut(),
            lessest: ptr::null_mut(),
            next_timer_id: 0,
            delay_id: 0,
            min_step: 0,
            all_deltatime: 0,
            len: 0,
        }
    }
}
设置时轮

时轮从大到小添加,如时钟的模型先添加12小时每个刻度为3600秒。

let mut timer = TimerWheel::new();
timer.append_timer_wheel(12, 60*60, "HourWheel");
timer.append_timer_wheel(60, 60, "MinuteWheel");

接下来添加分钟的轮,槽位60个,每个槽位60秒:

timer.append_timer_wheel(60, 60, "MinuteWheel");

接下来添加秒钟的轮,槽位60个,每个槽位1秒:

timer.append_timer_wheel(60, 1, "SecondWheel");

这里必须保证,每个小的刻度的总和必须为上一个模型的一个小刻度。
完整源码,将在添加的时候自动维护最小刻度及下一个delay_id防止空转。

pub fn append_timer_wheel(&mut self, slots: usize, step: usize, name: &'static str) {
    debug_assert!(self.len == 0, "必须时轮为空才可改变时轮");
    let one = Box::into_raw(Box::new(OneTimerWheel::new(slots, step, name)));
    self.delay_id = self.delay_id.max(slots * step);
    self.lessest = one;
    if self.greatest.is_null() {
        self.greatest = one;
    } else {
        unsafe {
            (*self.greatest).append(one);
        }
    }
    self.min_step = step;
}
添加元素

元素必须实现Timer,以确定放在指定的槽位中,将会返回定时器的id。以方便操作定时器,删除、获取、修改等操作。并且如果当前的时移更小,会重新设置delay_id的大小,以确定指定时间内能触发定时器。

pub fn add_timer(&mut self, mut val: T) -> usize {
    debug_assert!(!self.greatest.is_null(), "必须设置时轮才能添加元素");
    let timer_id = self.next_timer_id;
    self.next_timer_id += 1;
    self.delay_id = self.delay_id.min(val.when_mut());
    let entry = Entry { val, id: timer_id };
    unsafe {
        (*self.greatest).add_timer(entry);
    }
    self.len += 1;
    timer_id
}
确定时移

时移通过外部系统传入进去,可以方便的设置自己是否根据时间来进行判定。或者自己有另一套时移模式均可通用。

pub fn update_deltatime(&mut self, delta: usize) -> Option<Vec<T>>

如果当前的时移未大于delay_id,那么即表示还未触发任何的定时器。此时处理的仅仅为all_deltatime的自加,效率极高。
当时移大于delay_id后表示将触发定时器,与最小的min_step相除得出时轮中的时移offset

self.all_deltatime += delta;
let mut offset = self.all_deltatime / self.min_step;
if offset < self.delay_id {
    return None;
}
self.all_deltatime -= offset * self.min_step;

确定后将开始自身的时移,这里用比较高效的方式维护各个时轮:
比如以标准时钟为例,当前offset为156,即表示此时已过了156秒,那么秒轮的将会产生156 % 60 = 36的偏差值,那么秒轮的index=index+36为新的秒轮,且因为156 / 60 > 2,已经完整的转了一圈,表示秒轮上的所有节点全部被触发了。
此时分轮上移动了2分钟,比如此时为3,也就是index=index+2,那么移动的两个位置(3和4)所有元素将会被触发。此时分针指向了5,因为该位置上的最大偏移已经不超过60秒,为了使接下来在60秒内能正确触发该对象的值。那么将5身上的所有元素移除重新加入到了秒轮。

let mut result = vec![];
let mut wheel = self.lessest;
while !wheel.is_null() {
    unsafe {
        (offset, remainder) = (*wheel).update_index(offset, remainder, &mut result);
        if offset == 0 {
            break;
        }
        wheel = (*wheel).parent;
    }
}

更新索引值update_index,因为每次递进的的梯度可能会非常的大的,也可能会非常的小,这边需要准确的更新索引的值,并且包括降维的数据也能正确的降维。其中余数则是高维往低维降维的关键,例如149,从秒轮上来可以看出分轮有2分钟29秒,将会把第3分钟的进行降维,其中第三分钟如果在29内的标签应该是均命中,大于29的应该-29后的值插入到秒轮。

pub fn update_index(&mut self, offset: usize, remainder: usize, result: &mut Vec<T>) -> (usize, usize) {
    let next = self.index + offset;
    let mut all = 0;
    for idx in self.index..next {
        if all > self.capation {
            break;
        }
        all += 1;
        let idx = idx % self.capation;
        let list = &mut self.slots[idx];
        for val in list.drain(..) {
            result.push(val.val);
        }
    }
    self.index = next % self.capation;
    if !self.child.is_null() {
        unsafe {
            let list = &mut self.slots[self.index];
            for mut val in list.drain(..) {
                val.when = (val.when % self.step).saturating_sub(remainder);
                if val.when <= 0 {
                    result.push(val.val);
                } else {
                    (*self.child).add_step_timer(val);
                }
            }
        }
    }
    (next / self.capation, next % self.capation + remainder)
}

此时我们收集到了时移产生156秒触发的所有元素,我们将其打包进行返回,与此同时因为时轮上的指针发生变化,及元素被重新拆解,所以此时需要重新维护delay_id,此时需调用

pub fn calc_delay_id(&mut self)

密度高的基本为O(1)的复杂度,最差情况为O(n)的复杂度
总刻度数以时钟为计秒轮遍历60次,分轮遍历60次,时轮遍历12次,即最高遍历132次。

确定时移的时候也可以调用回调函数版本直接处理时间回调

pub fn update_deltatime_with_callback<F>(&mut self, delta: usize, f: &mut F) 
where 
    F: FnMut(&mut Self, T),
移除定时器

当我们对某个定时器不需要的时候可以提前进行删除

pub fn del_timer(&mut self, timer_id: usize) -> Option<T>

该模型删除不具备优势,时间复杂度为O(n),n为当前容器内的个数。
需要频繁删除请选用其它时间框架。如红黑树模型(nginx)的删除复杂度仅为O(logn)。

参考示例

以下是按时钟的一个计时轮:

use algorithm::TimerWheel;

fn main() {
    let mut timer = TimerWheel::new();
    timer.append_timer_wheel(12, 60 * 60, "HourWheel");
    timer.append_timer_wheel(60, 60, "MinuteWheel");
    timer.append_timer_wheel(60, 1, "SecondWheel");

    timer.add_timer(30);
    assert_eq!(timer.get_delay_id(), 30);
    timer.add_timer(149);
    assert_eq!(timer.get_delay_id(), 30);
    let t = timer.add_timer(600);
    assert_eq!(timer.get_delay_id(), 30);
    timer.add_timer(1);
    assert_eq!(timer.get_delay_id(), 1);
    timer.del_timer(t);
    timer.add_timer(150);
    assert_eq!(timer.get_delay_id(), 1);

    let val = timer.update_deltatime(30).unwrap();
    assert_eq!(val, vec![1, 30]);

    timer.add_timer(2);

    let val = timer.update_deltatime(119).unwrap();
    assert_eq!(val, vec![2, 149]);

    let val = timer.update_deltatime(1).unwrap();
    assert_eq!(val, vec![150]);
    
    assert!(timer.is_empty());
}

完整项目地址

https://github.com/tickbh/algorithm-rs

结语

TimerWheel算法通过其独特的数据结构和运行原理,实现了高效、可扩展且灵活的定时任务管理。该结构用于对高性能的定时器框架,尤其密集程度越高的定时器效率越高。

标签:mut,self,timer,60,源码,usize,TimerWheel,id,Rust
From: https://www.cnblogs.com/wmproxy/p/18243240/TimerWheel

相关文章

  • 龙哥量化:通达信空信号,可以买入翻倍的指标公式源码
    如果您需要代写公式,请联系我。龙哥QQ:591438821龙哥微信:Long622889  {当多空线上穿0轴以后并且沿着45度向上运行时可视为有效突破,此时的信号可视为有效信号。信号出现在平台盘整期间,或者是小多头回调之后向上拉升之际,此时的信号最为有效,其它时间的信号要仔细辨别,下跌趋势......
  • 龙哥量化:通达信筹码操盘,筹码来的副图源码
    如果您需要代写公式,请联系我。龙哥QQ:591438821龙哥微信:Long622889 DRAWGBK(ISLASTBAR,RGB(60,60,60),RGB(0,0,0),0,'0',0);机构控盘区:160,COLORMAGENTA,LINETHICK1;DRAWTEXT(ISLASTBAR,机构控盘区,'--←机构控盘区'),COLORMAGENTA;主升浪:150,COLORRED,LINETHICK1......
  • 龙哥量化:通达信决胜千里主图指标公式源码
    如果您需要代写公式,请联系我。龙哥QQ:591438821龙哥微信:Long622889B1:=EMA(C,2);紫1:=EMA(B1,2)COLORMAGENTA;色1:=EMA(紫1,2)COLORMAGENTA;线1:=EMA(色1,2)COLORMAGENTA;浮盈线:=EMA(紫1,5)COLORLIMAGENTA,LINETHICK2;成本线:=EMA(色1,13)COLORYELLOW,LINETHICK2;限时:=1;重......
  • 龙哥量化:诺曼底防线副图指标公式源码
    如果您需要代写公式,请联系我。龙哥QQ:591438821龙哥微信:Long622889VAR1:=(HHV(H,13)-LLV(L,13));VAR2:=(HHV(H,13)-C);VAR3:=(C-LLV(L,13));VAR4:=VAR2/VAR1*100-70;VAR5:=(C-LLV(L,55))/(HHV(H,55)-LLV(L,55))*100;VAR6:=(2*C+H+L)/4;VAR7:=SMA((VAR3/VAR1*100),3,1);VAR8:=......
  • Rustdesk 自建服务器
    自建rustdesk远程终端服务器,解决稳定远程控制需求。1、购买腾讯云服务器,99一年2、修改登录终端用户,默认使用ubuntu账户登录,存在权限限制,不能够上传文件Ubuntu系统如何使用root用户登录实例?(https://cloud.tencent.com/document/product/213/17278#ubuntu-.E7.B3.BB.E7.......
  • 短视频评论截流源码开发思路C#
    源码图镇帖一:准备工作这里我们采用了云技术,并不是通过客户端进行评论数据提取1.1:UI自动操作技术UI自动操作技术,顾名思义就是无需人工干预通过代码的方式让软件进行模拟人工操作,认为是脚本也好什么都可以。这和脚本还不一样,这里就不细说了。为什么获取短视频评论需要自动......
  • 行行比较,高逼格的SQL写法!【送源码】
    环境准备数据库版本:MySQL5.7.20-log建表SQLDROPTABLEIFEXISTS`t_ware_sale_statistics`;CREATETABLE`t_ware_sale_statistics`(`id`bigint(20)NOTNULLAUTO_INCREMENTCOMMENT'主键id',`business_id`bigint(20)NOTNULLCOMMENT'业务机构编码',......
  • 温泉镇旅游微信小程序的设计与实现(论文+源码)_kaic
    摘要旅游业随着经济的快速发展呈现出一派欣欣向荣的景象,尤其是近两年来,各个行业运用科技以及因特网来促进旅游迅速发展,逐渐都显示出了的问题,特别突出的是在线上推广,其缺点也是特别明显。尽管在新冠肺炎的冲击下,许多重要的旅游胜地和娱乐场所都被关闭,但是我认为,在未来,我国会在......
  • C#实现多线程的几种方式(附完整源码)
    C#实现多线程的几种方式1.使用Thread类:2.使用ThreadPool类:3.使用Task类:以下是C#中实现多线程的几种常见方式的示例代码:1.使用Thread类:usingSystem;usingSystem.Threading;​classProgram{staticvoidMain(){Threadth......
  • C#实现应用程序多屏显示(附完整源码)
    C#实现应用程序多屏显示下面是一个简单的C#示例程序,演示如何在多个屏幕上显示窗口。这个示例将创建两个窗体,并将它们分别显示在两个不同的屏幕上。如果你的系统上有多个屏幕,这个程序将会有效。确保你在一个WindowsForms应用程序中使用以下代码。首先,创建一个新的W......