【仿muduo库实现并发服务器】实现时间轮定时器

news2025/6/3 20:29:43

实现时间轮定时器

  • 1.时间轮定时器原理
  • 2.项目中实现目的
  • 3.实现功能
    • 3.1构造定时任务类
    • 3.2构造时间轮定时器
      • 每秒钟往后移动
      • 添加定时任务
      • 刷新定时任务
      • 取消定时任务
  • 4.完整代码

1.时间轮定时器原理

时间轮定时器的原理类似于时钟,比如现在12点,定一个3点的闹钟,那么过了三小时后,闹钟就会响起。
我们就可以定义一个时间数组,并有一个指针tick,指向数组的起始位置,每个元素下位置代表着具体时间,比如第二个元素位置为第一秒(起始位置为0秒),第三个元素位置表示第二秒,…第60个元素位置代表60秒。而tick指针每秒钟都向后移动一步,代表过了1秒。而走到哪里,就表示 哪里的任务该被执行了。

当我想要实现一个5秒后要执行的定时器,只需要将该定时器放入数组tick+5的位置去。tick指针每秒钟会向后移动一步,当5秒后,会走到对应的位置,这时去执行对应的定时任务即可。
在这里插入图片描述

不过同一时间可能会有多个定时任务需要执行,所以可以定一个二维的数组。
每个时间可以存放多个定时任务。
在这里插入图片描述

不过这里时间到了需要主动去执行对应的任务,我们有更好的方法可以自动执行对应的任务。
【类的析构】:
我们利用类的析构自动会执行的特性,所以我们可以将定时任务弄成一个类,并将任务放在析构函数中,当对象销毁时,就会自动的执行析构函数里面的定时任务。

2.项目中实现目的

在该项目中需要定时器的主要目的是用来管理每个连接的生命周期的,因为有的连接是恶意的,长时间连接啥也不干,所以为了避免这种情况,规定当一个连接创建时,超过一定时间没有动静的,就主动释放掉该连接。这时候就需要设置一个固定时间后的定时销毁任务。
比如规定时间为30s,当一个连接超过30没有发送数据或接收数据就需要释放掉。

目的:希望非活跃的连接在N秒后被释放掉。

【刷新时长】
不过当一个连接在创建后(定时任务放入30s位置上)第10秒时发送了数据,该连接的存活时间就需要重新更新,在第40秒后再释放。也就是在40s的位置上再把该定时任务插入进去。

需要在一个连接有IO事件产生的时候,延迟定时任务的执行

如何实现呢?
【shared_ptr+weak_ptr】
shared_ptr中有一个计数器,当计数器为0时资源才会真正释放。
只要让时间轮定时器里存储的是指向定时任务对象的智能指针shared_ptr,而不是定时器对象,这样就可以把要更新后的定时任务再插入进去。
这时候shared_ptr的计数器就为2,当tick走过30秒时对应的销毁任务不会执行,只会将计数器–变为1,而走到40s时,对应的销毁任务才会执行,因为这时候shared_ptr的计数器就为0了。

基于这个思想,我们可以使用shared_ptr来管理定时器任务对象

不过要主要需要使用weak_ptr来保存插入到时间轮里的定时任务对象信息。因为weak_ptr是弱引用,它不会增加shared_ptr的计数,还可以获取对应的shared_ptr对象。

  1. 首先第一个将任务封装起来,让这个任务呢在一个对象析构的
    时候再去执行它。
    2.而这个对象呢,使用shared_ptr来管理起来,添加定时任务只是添加了我们的一个shared_ptr的一个ptr对象。
    3.当要延迟一个任务的执行只需要针对这个任务呢?再去重新生成shared_ptr,添加到时间轮里边。
    4.该任务的计数器,就变就会加1,当前面的shared_ptr就算释放的也不会去释放所管理的对象那么,只有到后边的这个shared_ptr释放的时候计数为O了,才会去释放所管理的定时器任务。
    在这里插入图片描述

3.实现功能

时间轮定时器的主要功能有:添加定时任务;刷新定时任务;取消定时任务;

3.1构造定时任务类

1.将定时任务封装到一个类中,每个定时任务都有自己的的标识id,用它可以在时间轮中找到对应的定时器任务对象。
2.将定时任务的函数放在该类的析构函数中,当对象销毁时自动执行,要定时的任务由由用户指定所以通过回调函数_task_cb设置进去。
3.每个定时任务都有自己的超时时间timeout,当超过该时间就去执行该任务。
4.因为时间轮定时器中还需要保存每个定时器对象的weak_ptr,用来刷新定时任务,使用unordered_map来管理。通过定时器任务id找到对应的定时器weak_ptr对象。而当定时器对象销毁时,还需要将该对象的weak_ptr信息从map表中移除。这个操作是需要在时间轮定时器中实现的,所以是需要使用回调函数_release_cb,在时间轮定时器中设置进去。
5.取消定时任务就是不执行析构函数中的回调函数即可。通过一个布尔值设置。

#include <iostream>
#include <vector>
#include <unordered_map>
#include <memory>
#include <stdint.h>
#include <functional>
#include <unistd.h>
using TaskFunc= std::function<void()>;
using ReleaseFunc=std::function<void()>;
class TimerTask
{
public:
     //构造
     TimerTask(uint64_t id,uint32_t timeout,TaskFunc &cb):_id(id),_timeout(timeout),_canceled(false),_task_cb(cb){}
     
     //析构,当对象释放时执行定时任务,并且从timerwheel中移除该定时任务的信息
     ~TimerTask()
     {
        if(!_canceled)
        {_task_cb();_release_cb();}
     }
     //获取该定时器的超时时间
     uint32_t GetTimeout() {return _timeout;}
     void canceled() { _canceled = true; }/*取消定时器任务*/ 
     void ReleaseTask(ReleaseFunc cb)
     {
        _release_cb=cb;
     }  
private:
    
    uint64_t _id; //标识一个定时器对象
    uint32_t _timeout; //定时器的超时时间
    TaskFunc _task_cb; //要执行的定时任务
    bool _canceled;//定时任务默认是启动的,false为启动,true为终止定时器
    ReleaseFunc _release_cb;//释放时要从timerwheel中移除该定时器信息

};

3.2构造时间轮定时器

1.时间轮定时器我们通过vector来模拟二维数组。
2.而时间轮定时器中存储的是shared_ptr对象(指向定时器任务对象的智能指针)
3.时间轮定时器需要有一个tick,就是一个滴答指针,每秒钟向后移动一步,代表过了一秒。
4.时间轮定时器中还需呀一个哈希表管理着所有插入进来的定时任务对象的weak_ptr对象。通过定时任务的id来映射找到(当添加定时任务时,就会将id和对应的weak_ptr对象插入进去)

每秒钟往后移动

定时器启动后,tick指针每秒钟都要往后移动一步。tick走到哪,就代表对应位置的任务要被执行,执行的原理就是将对应位置管理资源的shared_ptr全部清除,那么shared_ptr销毁后—>定时器对象销毁---->执行析构函数中的任务。

   void RunTime()
    {
        _tick=(_tick+1)%_capacity;
        _wheel[_tick].clear();//将当前位置上的所有任务都释放掉,也就是都执行掉。
    }

添加定时任务

1.添加一个定时任务时,外部会给定这个定时器任务的id,超时时间和执行方法。
所以首先要根据这些构造一个shared_ptr对象。然后将释放函数设置到定时任务中。
2.插入的位置是所在tick基础上再向后移动timeout位置。
3.插入到时间轮里
4.将该定时任务信息以WeakPtr形式保存一份在map中。

 void SetRelease(uint64_t id)
    {
        auto it=_timers.find(id);
        if(it==_timers.end())return;
        _timers.erase(it);
    }
    //添加定时任务
    void AddTask(uint64_t id,uint32_t timeout,TaskFunc cb)
    {
        //首先构建一个shared_ptr类型的定时器任务
        PtrTask pt(new TimerTask(id,timeout,cb));
        //将释放函数内置进去
        pt->ReleaseTask(std::bind(&TimerWheel::SetRelease,this,id));
       
        int pos=(_tick+timeout)%_capacity;
        
        //插入到时间轮中
        _wheel[pos].push_back(pt);

        //再将该定时器任务保存一份信息在timers中
        _timers[id]=WeakTask(pt);
    }

刷新定时任务

当需要对定时任务进行延迟时,只需要根据该定时任务的id,去map表里找对应weak_ptr对象,并从weak_ptr对象中获取对应的shared_ptr对象,然后再在tick的基础上加上该定时器的超时时间,插入到时间轮里即可。

//刷新定时任务
    void RefreshTask(uint64_t id)
    {
        auto it=_timers.find(id);
        if(it==_timers.end())return;

        PtrTask pt=_timers[id].lock();//获取weakptr保存的shared_ptr

        uint32_t delay=pt->GetTimeout();

        int pos=(_tick+delay)%_capacity;

        _wheel[pos].push_back(pt);
    }

取消定时任务

要取消一个定时任务,只需要根据该定时任务的id到map表中找打它的weak_ptr对象,然后转换为shared_ptr对象,执行对应的终止函数即可。

  void CancelTimer(uint64_t id)
    {
        auto it=_timers.find(id);
        if(it==_timers.end())return;

        PtrTask pt=_timers[id].lock();//获取weakptr保存的shared_ptr
        pt->canceled();

    }

4.完整代码


#include <iostream>
#include <vector>
#include <unordered_map>
#include <memory>
#include <stdint.h>
#include <functional>
#include <unistd.h>

using TaskFunc= std::function<void()>;
using ReleaseFunc=std::function<void()>;
class TimerTask
{
public:
     //构造
     TimerTask(uint64_t id,uint32_t timeout,TaskFunc &cb):_id(id),_timeout(timeout),_canceled(false),_task_cb(cb){}
     
     
     //析构,当对象释放时执行定时任务,并且从timerwheel中移除该定时任务的信息
     ~TimerTask()
     {
        if(!_canceled)
        {_task_cb();_release_cb();}
     }
     
     //获取该定时器的超时时间
     uint32_t GetTimeout() {return _timeout;}
     void canceled() { _canceled = true; }/*取消定时器任务*/ 

     void ReleaseTask(ReleaseFunc cb)
     {
        _release_cb=cb;
     }  
     

private:
    
    uint64_t _id; //标识一个定时器对象
    uint32_t _timeout; //定时器的超时时间
    TaskFunc _task_cb; //要执行的定时任务
    bool _canceled;//定时任务默认是启动的,false为启动,true为终止定时器
    ReleaseFunc _release_cb;//释放时要从timerwheel中移除该定时器信息

};

class TimerWheel
{
    
    using PtrTask=std::shared_ptr<TimerTask>;
    using WeakTask=std::weak_ptr<TimerTask>;
public:
    //构造
    TimerWheel():_tick(0),_capacity(60),_wheel(_capacity){}
    
    void SetRelease(uint64_t id)
    {
        auto it=_timers.find(id);
        if(it==_timers.end())return;
        _timers.erase(it);
    }
    //添加定时任务
    void AddTask(uint64_t id,uint32_t timeout,TaskFunc cb)
    {
        //首先构建一个shared_ptr类型的定时器任务
        PtrTask pt(new TimerTask(id,timeout,cb));
        //将释放函数内置进去
        pt->ReleaseTask(std::bind(&TimerWheel::SetRelease,this,id));
       
        int pos=(_tick+timeout)%_capacity;
        
        //插入到时间轮中
        _wheel[pos].push_back(pt);

        //再将该定时器任务保存一份信息在timers中
        _timers[id]=WeakTask(pt);
    }
    //刷新定时任务
    void RefreshTask(uint64_t id)
    {
        auto it=_timers.find(id);
        if(it==_timers.end())return;

        PtrTask pt=_timers[id].lock();//获取weakptr保存的shared_ptr

        uint32_t delay=pt->GetTimeout();

        int pos=(_tick+delay)%_capacity;

        _wheel[pos].push_back(pt);
    }
    void CancelTimer(uint64_t id)
    {
        auto it=_timers.find(id);
        if(it==_timers.end())return;

        PtrTask pt=_timers[id].lock();//获取weakptr保存的shared_ptr
        pt->canceled();

    }
    void RunTime()
    {
        _tick=(_tick+1)%_capacity;
        _wheel[_tick].clear();//将当前位置上的所有任务都释放掉,也就是都执行掉。
    }

private:
    int _tick; //滴答指针,指向哪就执行对应的任务,也就是释放该任务对象
    int _capacity; //定时器时间轮的容量大小
    std::vector<std::vector<PtrTask>> _wheel;//时间轮里存的是指向定时器任务对象的智能指针
    std::unordered_map<uint64_t,WeakTask> _timers;//存储时间轮里的定时器信息
};





class Test
{
public:
    Test()
    {
        std::cout<<"构造"<<std::endl;
    }
    ~Test()
    {
         std::cout<<"析构"<<std::endl;
    }

};

//测试
void Delete(Test* t)
{
    delete t;
}
int main()
{
   Test* t=new Test();
   

   TimerWheel tw;
   tw.AddTask(888,5,std::bind(Delete,t));

   for(int i=0;i<5;i++)
   {
      std::cout<<"---------------------"<<std::endl;
      tw.RefreshTask(888);
      tw.RunTime();
      sleep(1);
   }
   for(int i=0;i<5;i++)
   {
      std::cout<<"---------------------"<<std::endl;

      tw.RunTime();
      sleep(1);
   }
}

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

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

相关文章

day15 leetcode-hot100-28(链表7)

2. 两数相加 - 力扣&#xff08;LeetCode&#xff09; 1.模拟 思路 最核心的一点就是将两个链表模拟为等长&#xff0c;不足的假设为0&#xff1b; &#xff08;1&#xff09;设置一个新链表newl来代表相加结果。 &#xff08;2&#xff09;链表1与链表2相加&#xff0c;具…

​​知识图谱:重构认知的智能革命​

在数字经济的浪潮中&#xff0c;知识图谱正悄然掀起一场认知革命。它不仅是技术的迭代&#xff0c;更是人类从“数据依赖”迈向“知识驱动”的里程碑。当谷歌用知识图谱优化搜索引擎、银行用它穿透复杂的金融欺诈网络、医院用它辅助癌症诊疗时&#xff0c;这项技术已悄然渗透到…

【计算机网络】4网络层①

这篇笔记讲IPv4和IPv6。 为了解决“IP地址耗尽”问题,有三种措施: ①CIDR(延长IPv4使用寿命) ②NAT(延长IPv4使用寿命) ③IPv6(从根本上解决IP地址耗尽问题) IPv6 在考研中考查频率较低,但需掌握基础概念以防冷门考点,重点结合数据报格式和与 IPv4 的对比记忆。…

MATLAB中的table数据类型:高效数据管理的利器

MATLAB中的table数据类型&#xff1a;高效数据管理的利器 什么是table数据类型&#xff1f; MATLAB中的table是一种用于存储列向数据的数据类型&#xff0c;它将不同类型的数据组织在一个表格结构中&#xff0c;类似于电子表格或数据库表。自R2013b版本引入以来&#xff0c;t…

Dropout 在大语言模型中的应用:以 GPT 和 BERT 为例

引言 大型语言模型&#xff08;LLMs&#xff09;如 GPT&#xff08;生成式预训练 Transformer&#xff09;和 BERT&#xff08;双向编码器表示 Transformer&#xff09;通过其强大的语言理解和生成能力&#xff0c;彻底改变了自然语言处理&#xff08;NLP&#xff09;领域。然…

gitLab 切换中文模式

点击【头像】--选择settings 选择【language】,选择中文&#xff0c;点击【保存】即可。

133.在 Vue3 中使用 OpenLayers 实现画多边形、任意编辑、遮罩与剪切处理功能

&#x1f3ac; 效果演示截图&#xff08;先睹为快&#xff09; ✨ 功能概览&#xff1a; ✅ 鼠标画任意形状多边形&#xff1b; ✏️ 点击“修改边界”可拖动顶点&#xff1b; &#x1f7e5; 点击“遮罩”后地图除多边形区域外变红&#xff1b; ✂️ 点击“剪切”后仅显示选…

4.8.4 利用Spark SQL实现分组排行榜

在本次实战中&#xff0c;我们的目标是利用Spark SQL实现分组排行榜&#xff0c;特别是计算每个学生分数最高的前3个成绩。任务的原始数据由一组学生成绩组成&#xff0c;每个学生可能有多个成绩记录。我们首先将这些数据读入Spark DataFrame&#xff0c;然后按学生姓名分组&am…

【五子棋在线对战】一.前置知识的了解

前置知识的了解 前言1.Websocketpp1.1 使用Websocketpp的原因1.2 Websocket常用接口1.3 Websocket搭建服务器流程 2.JsonCpp2.1 Json 数据对象类的表示2.2序列化和反序列化的接口2.3 演示代码 3.Mysql![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/93305f423b544fc1…

历年中国科学技术大学计算机保研上机真题

2025中国科学技术大学计算机保研上机真题 2024中国科学技术大学计算机保研上机真题 2023中国科学技术大学计算机保研上机真题 在线测评链接&#xff1a;https://pgcode.cn/school?classification1 拆分数字 题目描述 给定一个数字&#xff0c;拆分成若干个数字之和&#xff…

HackMyVM-Art

信息搜集 主机发现 ┌──(kali㉿kali)-[~] └─$ nmap -sn 192.168.43.0/24 Starting Nmap 7.95 ( https://nmap.org ) at 2025-05-31 03:00 EDT Nmap scan report for 192.168.43.1 Host is up (0.0047s latency). MAC Address: C6:45:66:05:91:88 (Unknown) Nmap scan rep…

网页前端开发(基础进阶1)

颜色表示方法3种&#xff1a; 1.关键字&#xff1a; color&#xff1a;green&#xff1b; gray red yellow 2.rgb表示法&#xff1a;红&#xff0c;绿&#xff0c;蓝三原色。rgb&#xff08;r&#xff0c;g&#xff0c;b&#xff09;&#xff0c;r表示红色&#xff0c;g表示绿…

如何找到一条适合自己企业的发展之路?

一个创业型的企业&#xff0c;开始就需要面向市场&#xff0c;通过自己的服务或产品&#xff0c;帮助用户解决问题&#xff0c;为客户创造价值&#xff0c;通过为客户创造的价值&#xff0c;出创造一定的的现金流&#xff0c;让企业存活下来&#xff01; 企业的运营过程中&…

Vue-数据监听

数据监听 基础信息 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><title>数据监听</title><!-- 引入Vue --><script type"text/javascript" src"../js/vue.js&qu…

当前用户的Git全局配置情况:git config --global --list

通过config命令可以查询当前用户的全局配置情况。这些配置项定义了 Git 在全局范围内的行为&#xff0c;包括如何处理大文件、SSL 证书验证以及提交时的用户信息。 git config --global --list http.sslVerifyfalse 这个配置项禁用了 SSL 证书验证。这在与自签名证书的 Git 服…

AI生态警报:MCP协议风险与应对指南(中)——MCP Server运行时安全​​

作为连接AI模型与外部工具的“USB-C接口”&#xff0c;MCP协议成为AI生态的核心枢纽&#xff0c;其安全风险已从理论威胁转化为实际攻击目标。 AI生态警报&#xff1a;MCP协议风险与应对指南&#xff08;上&#xff09;——架构与供应链风险https://blog.csdn.net/WangsuSecur…

day15 leetcode-hot100-29(链表8)

19. 删除链表的倒数第 N 个结点 - 力扣&#xff08;LeetCode&#xff09; 1.暴力法 思路 &#xff08;1&#xff09;先获取链表的长度L &#xff08;2&#xff09;然后再次遍历链表到L-n的位置&#xff0c;直接让该指针的节点指向下下一个即可。 2.哈希表 思路 &#xff0…

MonitorSDK_性能监控(从Web Vital性能指标、PerformanceObserver API和具体代码实现)

性能监控 性能指标 在实现性能监控前&#xff0c;先了解Web Vitals涉及的常见的性能指标 Web Vitals 是由 Google 推出的网页用户体验衡量指标体系&#xff0c;旨在帮助开发者量化和优化网页在实际用户终端上的性能体验。Web Vitals 强调“以用户为中心”的度量&#xff0c;而不…

LeeCode 98. 验证二叉搜索树

给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下&#xff1a; 节点的左子树只包含 小于 当前节点的数。节点的右子树只包含 大于 当前节点的数。所有左子树和右子树自身必须也是二叉搜索树。 提示&#xff1a; 树中节…

JVM类加载高阶实战:从双亲委派到弹性架构的设计进化

前言 作为Java开发者&#xff0c;我们都知道JVM的类加载机制遵循"双亲委派"原则。但在实际开发中&#xff0c;特别是在金融支付、插件化架构等场景下&#xff0c;严格遵循这个原则反而会成为系统扩展的桎梏。本文将带你深入理解双亲委派机制的本质&#xff0c;并分享…