时间轮和时间堆管理定时器

news2024/11/18 5:56:46

高性能定时器

时间轮

由于排序链表定时器容器有这样一个问题:添加定时器的效率偏低。而即将介绍的时间轮则解决了这个问题。一种简单的时间轮如下所示。

在这里插入图片描述

如图所示的时间轮内,指针指向轮子上的一个slot(槽), 它以恒定的时间顺时针转动,每转动一步指向下一个槽,每次转动就称为一个滴答即tick()一个tick的时间成为时间轮的槽间隔时间si(slot interval)。该时间轮共有N个槽,因此它运转一周的时间为 N * si

每个槽指向一条定时器链表,每条链表上的定时器都具有相同的特征:它们的定时时间相差 N * si 的整数倍。

假如现在指针指向槽cs(current slot), 添加一个定时时间为ti的定时器,则该定时器将被插入槽 ts(timer slot)对应的链表中:

ts = (cs + (ti / si)) % N

基于排序链表的定时器使用唯一的一条链表来管理所有定时器,所以插入操作随着定时器数目的增多而降低。

时间轮使用哈希表的思想,将定时器散列到不同的链表上。 这样每条链表上的定时器数目都将明显少于原来的排序链表上的定时器数目。

对于时间轮而言,要提高定时精度,就要使**si (槽间隔)**值足够小;

要提高执行效率,则要求**N(槽数量)**值足够大。

如下是一个简单的时间轮代码,本上还是以链表的操作为主

// 时间轮定时器,用数组(环)存储每一条定时器链表
// 求hash(超时时间)决定定时器到数组的哪个位置
#ifndef TIME_WHEEL_TIMER
#define TIME_WHEEL_TIMER

#include <time.h>
#include <netinet/in.h>
#include <stdio.h>

#define BUFFER_SIZE 64
class tw_timer; // 时间轮定时器前置声明

// 绑定socket和定时器
struct client_data
{
    sockaddr_in address;
    int sockfd;
    char buf[BUFFER_SIZE];
    tw_timer *timer;
}

class tw_timer
{
public:
    tw_timer(int rot, int ts)
    :next(NULL), prev(NULL), rotation(rot), time_slot(ts){}

public:
    int rotation; // 记录定时器在时间轮转多少圈才生效
    int time_slot; // 记录定时器属于时间轮上的哪个slot
    void (*cb_func)(client_data *);
    client_data *user_data; // 用指针存储对应的用户数据
    tw_timer *next;
    tw_timer *prev; 
}

class time_wheel
{
public:
    time_wheel() : cur_slot(0)
    {
        for(int i = 0; i < N; ++i)
        {
            slots[i] = NULL; // init
        }
    }
    ~time_wheel()
    {
        for(int i = 0; i < N; ++i)
        {
            tw_timer *tmp = slots[i];
            while(tmp)
            {
                // 重新设置链表头节点
                slots[i] = tmp->next;
                delete tmp;
                tmp = slots[i];
            }
        }
    }

    // 根据定时值timeout,创建一个定时器,并把它插入到合适的槽中
    tw_timer* add_timer(int timeout)
    {
        if(timeout < 0)
        {
            return NULL;
        }
        int ticks = 0;
        // 根据待插入定时器的超时值计算它将在时间轮转动多少个滴答后被触发
        // 若待插入定时器的超时值小于时间轮的槽间隔,则将ticks向上取整为1
        if(timeout < SI)
        {
            ticks = 1;
        }
        else 
        {
            ticks = timeout / SI;
        }
        // 计算转多少圈后被触发
        int rotation = ticks / N;
        // 计算待插入的定时器应该被插入哪个槽中
        int ts = (cur_slot + (ticks % N)) % N;
        // 创建新的定时器,它在时间轮转动rotation圈之后触发,位于第ts个slot上
        tw_timer *timer = new tw_timer(rotation, ts);
        // 插入指定槽中的链表 头
        // 第ts个slot没有任何定时器(空链表)
        if(!slots[ts])
        {
            printf("add timer, rotation is %d, ts is %d, cur_slot is %d\n", rotation, ts, cur_slot);
        }
        else // 链表非空,则头插 
        {
            timer->next = slots[ts];
            slots[ts]->prev = timer;
            slots[ts] = timer;
        }
        return timer;
    }

    // 删除目标定时器
    void del_timer(tw_timer *timer)
    {
        if(!timer)
        {
            return;
        }
        int ts = timer->time_slot;
        // slots[ts] 是目标定时器所在头节点
        // 如果待删定时器就是该头节点,则需要重置第ts个slot的链表头节点
        if(timer == slots[ts])
        {
            slots[ts] = slots[ts]->next;
            if(slots[ts])
            {
                slots[ts]->prev = NULL;
            }
            // 如果第ts个slot的链表就剩下一个节点,直接删除
            delete timer;
        }
        else
        {
            timer->prev->next = timer->next;
            if(timer->next)
            {
                timer->next->prev = timer->prev;
            }
            delete timer;
        }

    }

    // 时间轮转动函数,每SI时间之后,cur_slot向前滚动一个slot
    void tick()
    {
        // 时间轮上当前槽的头节点
        tw_timer *tmp = slots[cur_slot];
        printf("current slot is %d\n", cur_slot);
        // 遍历当前slot上链表的每个定时器节点,
        while(tmp)
        {
           printf("tick the timer once\n"); 
           // 定时器超过1轮,跳过
           if(tmp->rotation > 0)
           {
               tmp->rotation--;
               tmp = tmp->next;
           }
           // 否则只要指针到当前slot,里面的所有定时器就都到时了
           else // 执行定时任务,删除tmp节点
           {
               tmp->cb_func(tmp->user_data);
               
               // 链表头节点!!
               if(tmp = slots[cur_slot])
               {
                   printf("delete header in cur_slot\n");
                   slots[cur_slot] = tmp->next; // 让tmp的下一个节点做头节点
                   delete tmp;
                   if(slots[cur_slot])
                   {
                       slots[cur_slot]->prev = tmp->prev;
                   }
                   tmp = slots[cur_slot]; // tmp为刚刚删除的节点的下一个节点
               }
               else // 非头节点
               {
                   tmp->prev->next = tmp->next;
                   if(tmp->next)
                   {
                       tmp->next->prev = tmp->prev;
                   }
                   tw_timer *tmp2 = tmp->next;
                   delete tmp;
                   tmp = tmp2; //
               }
           }
        }
        // 时间轮转动(指针移动到下一个slot)
        cur_slot = (++cur_slot) % N;
    }
private:
    // 时间轮上slot的数目
    static const int N = 60;
    // 指针每1s转动一次,即slot的间隔为1s   slot interval
    static const int SI = 1;
    // 时间轮,每个存放定时器链表
    tw_timer* slots[N];
    int cur_slot; // 指针,指向当前的slot
};

复杂度分析

由于添加一个定时器是链表头插,则时间复杂度为 O ( 1 ) O(1) O(1)

删除一个定时器的时间复杂的也为 O ( 1 ) O(1) O(1)

执行一个定时器的复杂度为 O ( n ) O(n) O(n).但实际执行一个定时任务效率要比 O ( n ) O(n) O(n)好,因为时间轮将所有定时器散列到不同的链表上。

若使用多个轮子实现时间轮,执行一个定时器任务的复杂度可以降到 O ( 1 ) O(1) O(1)

时间堆

前面讨论的定时器方案都是以固定的频率调用定时处理函数tick(),并在其中依次检测到期的定时器,然后执行到期定时器上的回调函数。

设计定时器的另一种思路是:将所有定时器中超时时间最小的一个定时器的超时时间作为调用tick()的间隔时间。 这样,一旦tick()函数被调用,超时时间最小的定时器必然到期,我们就可以在tick()函数中处理该定时器。

然后再从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次tick()间隔。

最小堆很适合这种定时方案。本文实现最小堆有以下关键特点:

  • 根节点值小于其孩子节点的值(递归成立);

  • 插入节点是在最后一个节点添加新节点,然后进行上滤保证最小堆特性;

  • 删除节点是删除其根节点上的元素,然后把最后一个元素移动到根节点,进行下滤操作保证最小堆特性;

  • 将N个元素的数组(普通二叉树)初始化为最小堆,即从二叉树最后一个非叶节点到根节点(第 [ ( N − 1 ) / 2 ] [(N-1) / 2] [(N1)/2] ~ 0 个元素)执行下滤操作。

  • 本文实现的最小堆底层是用数组进行存储,是一个适配器,联想C++的 priority_queue<int, vector<int>, greater<int>>

最小堆代码实现如下

// 用最小堆存储定时器,称为时间堆
#ifndef MIN_HEAP
#define MIN_HEAP

#include <iostream>
#include <netinet/in.h>
#include <time.h>
using std::exception;

#define BUFFER_SIZE 64
class heap_timer;

// 绑定socket和定时器
struct client_data
{
    sockaddr_in address;
    int sockfd;
    char buf[BUFFER_SIZE];
    heap_timer *timer;
};

// 定时器类
class heap_timer
{
public:
    heap_timer(int delay)
    {
        expire = time(NULL) + delay;
    }

public:
    time_t expire = expire; // 定时器生效的绝对时间
    void (*cb_func)(client_data *);
    client_data *user_data;
};

// 时间堆类
class time_heap
{
public: 
    // 初始化一个大小为cap的空堆
    // throw (std::exception) 表示该函数可能抛出std::exception 类型的异常
    time_heap(int cap) throw (std::exception) : capacity(cap), cur_size(0)
    {
        array = new heap_timer*[capacity];
        if(!array)
        {
            throw std::exception();
        }
        else
        {
            for(int i = 0; i < capacity; ++i)
            {
                array[i] = NULL;
            }
        }
    }

    // 用已有的堆数组初始化堆
    time_heap(heap_timer **init_array, int size, int capacity) throw (std::exception) : cur_size(size), capacity(capacity) 
    {
        if(capacity < size)
        {
            throw std::exception();
        }
        array = new heap_timer*[capacity];
        if(!array)
        {
            throw std::exception();
        }
        for(int i = 0; i < capacity; ++i)
        {
            array[i] = NULL;
        }
        if(size != 0)
        {
            // 初始化数组
            for(int i = 0; i < size; ++i)
            {
                array[i] = init_array[i];
            }
            // 最后一个非叶子节点到根节点调堆(下滤)
            for(int i = (cur_size - 1); i >= 0; --i)
            {
                percolate_down(i);
            }
        }
    }
    ~time_heap()
    {
        for(int i = 0; i < cur_size; ++i)
        {
            delete array[i];
        }
        delete []array;
    }

public:
    // 堆添加节点,上滤
    void add_timer(heap_timer *timer) throw (std::exception)
    {
        if(!timer)
        {
            return;
        }
        if(cur_size >= capacity) // 容量不足,堆指针数组需要扩充一倍
        {
            resize();
        }
        // 新插入了一个元素,在堆最后插入,然后调堆
        int hole = cur_size++;
        int parent = 0;
        // 上滤操作
        for(; hole > 0; hole = parent) // hole = parent使得最终结果位置上移
        {
            parent = (hole - 1) / 2; // hole节点的父节点计算
            if(array[parent]->expire <= timer->expire)
            {
                // 父节点小于插入的节点,满足小根堆要求,直接结束
                break;
            }
            array[hole] = array[parent]; // 父节点节点下移
        }
        array[hole] = timer;
    }
    void del_timer(heap_timer *timer)
    {
        if(!timer)
        {
            return;
        }
        // 仅仅将目标定时器的回调函数设置为空,即延迟销毁
        // 这将节省真正删除该定时器的开销,但易使堆数指针组膨胀
        timer->cb_func = NULL;
    }

    // 获取堆顶部的定时器,expire最小者
    heap_timer* top() const
    {
        if(empty())
        {
            return NULL;
        }
        return array[0];
    }

    // 删除堆顶部的定时器
    void pop_timer()
    {
        if(empty())
        {
            return ;
        }
        if(array[0])
        {
            delete array[0];
            // 将原来堆顶元素用堆的最后一个元素临时填充,然后下滤
            array[0] = array[--cur_size];
            percolate_down(0);
        }
    }

    // 定时处理函数
    void tick()
    {
        heap_timer *tmp = array[0];
        time_t cur = time(NULL); // 循环遍历堆中每个定时器(堆用数组实现,故数组遍历),处理到期的定时器
        while(!empty())
        {
            if(!tmp)
            {
                break;
            }
            // 如果堆顶定时器没到期,则退出循环,因为堆顶定时器到时时间使最近的,其他更晚
            if(tmp->expire > cur)
            {
                break;
            }
            if(array[0]->cb_func)
            {
                array[0]->cb_func(array[0]->user_data);
            }
            // 将堆顶元素删除,同时让tmp指向新的堆顶
            pop_timer();
            tmp = array[0];
        }
    }

    bool empty() const
    {
        return cur_size == 0;
    }

private:
    // 下面两个函数是被其他成员函数调用,不对外提供
    // 最小堆的下滤操作,确保数组中以第hole个节点作为根的子树满足最小堆性质
    void percolate_down(int hole)
    {
        heap_timer *temp = array[hole];
        int child = 0;
        // hole * 2 + 1为hole的左孩子
        for(; (hole * 2 + 1) <= (cur_size - 1); hole = child) // hole = child是一个下滤的动作
        {
            child = hole * 2 + 1; // 左孩子
            // 要选择expire小的孩子进行比较
            if((child < (cur_size - 1)) && (array[child + 1]->expire < array[child]->expire))
            {
                child++;
            }
            if(array[child]->expire < temp->expire) // 下滤
            {
                array[hole] = array[child];
            }
            else
            {
                break;
            }
        }
        array[hole] = temp;
    }

    // 将堆数组容量扩大一倍
    void resize() throw (std::exception)
    {
        heap_timer **temp = new heap_timer*[2 * capacity];
        for(int i = 0; i < 2 * capacity; ++i)
        {
            temp[i] = NULL;
        }
        if(!temp)
        {
            throw std::exception();
        }
        capacity = 2 * capacity;
        // 把原来数组的内容拷贝到新的数组
        for(int i = 0; i < cur_size; ++i)
        {
            temp[i] = array[i];
        }
        delete []array;
        array = temp;
    }
private:
    heap_timer **array; // 定时器指针数组
    int capacity;
    int cur_size;
};

#endif

复杂度分析

  • 对时间堆而言,添加一个定时器的时间复杂度为 O ( l o g n ) O(logn) O(logn)(由于需要上滤操作)

  • 删除一个定时器的时间复杂度为 O ( 1 ) O(1) O(1),这是因为只是将目标定时器的回调函数设置为空

  • 执行一个定时器的时间复杂度为 O ( 1 ) O(1) O(1)

参考文献

  1. 《Linux高性能服务器编程》,游双

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

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

相关文章

Linux操作系统-线程互斥,线程同步,生产者消费者模型

线程互斥线程互斥及相关概念线程互斥&#xff08;Mutual Exclusion&#xff09;是指在多线程环境下&#xff0c;同一时刻只能有一个线程访问共享资源&#xff0c;以避免对该资源的不正确访问&#xff0c;造成数据不一致等问题。例如&#xff0c;如果有多个线程都要同时对同一个…

web端元素各种尺寸示意图

1.偏移尺寸 offsetHeight 元素在垂直方向上占用的尺寸(height,border,水平滚动条高度) offsetWidth 元素在垂直方向上占用的尺寸(height,border,水平滚动条高度) offsetTop 元素上边框外侧距离包含元素上边框内侧的尺寸 offsetLeft 元素左边框外侧距离包含元素左边框内侧的尺寸…

Python-第八天 Python文件操作

Python-第八天 Python文件操作一、文件的编码1. 什么是编码&#xff1f;2. 为什么需要使用编码&#xff1f;二、文件操作1.文件的操作步骤2. 打开文件3.mode常用的三种基础访问模式4.关闭文件三、文件的读取1.文件对象有如下读取方法&#xff1a;2.练习&#xff1a;单词计数三、…

nextTick 的使用和原理(面试题)

答题思路&#xff1a; nextTick 是做什么的&#xff1f;为什么需要它&#xff1f;开发时什么时候使用&#xff1f;介绍一下如何使用nextTick原理解读&#xff0c;结合异步更新和nextTick生效方式 1. nextTick是做什么的&#xff1f; nextTick是等待下一次DOM更新刷新的工具方法…

电子电器架构——怎样在请求/响应 ID确定的情况下修改发送FD 的CAN ID?

我是穿拖鞋的汉子,魔都中一个坚持长期主义的工程师! 老规矩,分享一段喜欢的文字,避免成为高知识低文化的人: 能不传话,最好不要传话;能不套话,最好不要套话;能不涉入“背后的批评”,最好不要涉入。让自己像沙滩,多大的浪来了,也是轻抚着沙滩,一波波地退去。而不要…

Ubuntu 快速切换到指定目录

现有以下场景&#xff0c;假设我在本地有/home/pc/Downloads/temp/Project 目录&#xff0c;我想快速在终端进入Project目录&#xff0c;需要怎么操作呢 文件管理器 由于我知道这个目录在哪个位置&#xff0c;那我就可以打开文件管理器&#xff0c;进入到这个目录&#xff0c…

关于数据治理ChatGPT是如何回答的?

这两天你的朋友圈是不是被火爆全网的ChatGPT霸屏了&#xff1f;你是不是已经迫不及待感受过ChatGPT带来的惊喜&#xff1f;那你知道ChatGPT是什么吗&#xff1f;面对掀起的一波话题热潮&#xff0c;好奇使然&#xff0c;小编去特别关注了一下最近火热的ChatGPT&#xff0c;看看…

基于Spring cloud搭建oauth2

1&#xff0c;OAuth2.0简介 OAuth&#xff08;开发授权&#xff09;是一个开放标准&#xff0c;允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息&#xff0c;而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。 OAuth2.0是OAuth的延续&#xf…

预告| 亮点抢先看!第四届OpenI/O启智开发者大会主论坛24日启幕!

2023年2月24日至25日&#xff0c;第四届OpenI/O启智开发者大会将在深圳隆重举行。“算网筑基、开源启智、AI赋能”作为今年大会的主题&#xff0c;吸引了全球业界关注的目光。大会集结中国算力网资源基座、开源社区治理及AI开源生态建设、国家级开放创新应用平台、NLP大模型等前…

2023年云计算的发展趋势如何?还值得学习就业吗?

一、2023年云计算的发展将迎来新篇章 随着政策的正式放开&#xff0c;2023年的经济开始慢慢复苏&#xff0c;云计算在疫情期间支撑了复工复产&#xff0c;那么在今年对于云计算发展的限制将进一步的放开。Gartner的数据显示&#xff0c;到2023年&#xff0c;全球公共云支出将达…

MybatisPlus------条件构造器Wrapper以及QueryWrapper用法(七)

MybatisPlus------条件构造器Wapper&#xff08;七&#xff09; Wrapper:条件构造器抽象类&#xff0c;最顶端父类 AbstarctWrapper&#xff1a;用于查询条件封装&#xff0c;生成sql的where条件。 QueryWrapper&#xff1a;查询条件封装&#xff08;可以用于查询、删除&#x…

Java必备小知识点1

Java程序类型: Applications和AppletApplications:是指在计算机操作系统中运行的程序。是完整的程序&#xff0c;能独立运行。被编译后&#xff0c;用普通的Java解释器就可以使其边解释边执行。必定含有一个main方法&#xff0c;程序执行时&#xff0c;首先寻找main方法&#x…

IDEA中如何配置SpringBoot项目多实例不同端口运行

1 问题场景 我们在进行新项目开发的时候&#xff0c; 可能做完一个新的模块功能并自测通过之后&#xff0c; 我们希望测试人员能帮我跑一些单元测试用例来进行测试验证&#xff0c; 但是我们又需要在此基础上技术开发新的功能&#xff0c; 这是我们就需要在我们的开发PC上同时…

Prometheus监控案例-kube-state-metrics

kube-state-metrics组件介绍 kube-stste-metrics项目地址&#xff1a;https://github.com/kubernetes/kube-state-metrics kube-stste-metrics是一个简单的组件&#xff0c;通过监听API Server生成有关资源对象的状态指标&#xff08;例如Deployment、Pod、Node等&#xff09…

HiEV洞察 | 卖一台亏半台,激光雷达第一股禾赛隐忧仍在

作者 | 感知君Alex 编辑 | 王博2月9日晚&#xff0c;禾赛在万众瞩目下登陆纳斯达克&#xff0c;发行价19美元每股&#xff0c;首日涨超11%&#xff0c;市值超过Luminar&#xff0c;登顶全球市值最高的激光雷达公司。 随后两个交易日&#xff0c;其股价均有不同程度的涨幅&#…

08- 汽车产品聚类分析综合项目 (机器学习聚类算法) (项目八)

项目难点 主要通过聚类算法 kmeans 进行调整 . 需要找出分为几类时模型参数最佳 . (n_clusters)找出性价比较高的车 获取训练数据: train_X data.drop([car_ID,CarName],axis 1)计算模型的得分和误差: kmeans.inertia_ # inertia簇内误差平方和 from sklearn.cluster i…

【深度学习/机器学习】为什么要归一化?归一化方法详解

【深度学习/机器学习】为什么要归一化&#xff1f;归一化方法详解 文章目录1. 介绍1.1 什么是归一化1.2 归一化的好处2. 归一化方法2.1 最大最小标准化&#xff08;Min-Max Normalization&#xff09;2.2 Z-score标准化方法2.3 非线性归一化2.4 L范数归一化方法&#xff08;最典…

宝塔搭建实战人才求职管理系统admin前端vue源码(二)

大家好啊&#xff0c;我是测评君&#xff0c;欢迎来到web测评。 上一期给大家分享骑士cms后台端在宝塔的搭建部署方式&#xff0c;这套系统是前后端分离的架构&#xff0c;前端是用vue2开发的&#xff0c;还需要在本地打包手动发布上宝塔&#xff0c;所以本期给大家分享&#x…

智能笔式万用表简单体验加拆解 - VC6012C - 智能电笔

简而言之&#xff0c;能用&#xff0c;甚至还挺好用的&#xff0c;机身大小参考上面的示意图&#xff0c;跟比较粗的记号笔差不多。单纯想买个万用表的话&#xff0c;如果不追求这种精简的外形&#xff0c;同价位有其他功能更强的选项。其实就是个能自动切换档位的智能万用表加…

山东大学软件学院面向对象简答题整理【个人向】

面向对象简答题整理【个人向】 0.试用面向对象语言简述改写和重定义的异同&#xff0c;以及方法绑定时的差别 改写是子类的方法和父类的方法具有相同的方法名和类型签名重定义是子类的方法和父类的方法方法名相同但类型签名不同在方法绑定时&#xff0c;改写是动态绑定&#…