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

article/2025/7/29 18:12:41

实现时间轮定时器

  • 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_ptruint32_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_ptrpt->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_ptruint32_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_ptrpt->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.hkcw.cn/article/ccRCXKVTKa.shtml

相关文章

Webug4.0靶场通关笔记10- 第10关存储型XSS注入

目录 一、存储型XSS原理 二、代码审计 三、第10关 存储型XSS注入实战 1.打开靶场 2.渗透实战 本文通过《Webug4.0通关笔记系列》来进行Webug4.0靶场的渗透实战&#xff0c;本文讲解Webug4.0靶场第10关存储型XSS的渗透实战。 一、存储型XSS原理 存储型XSS&#xff08;Sto…

深入了解MCP基础与架构

一、引言 在人工智能技术以指数级速度渗透各行业领域的今天&#xff0c;我们正站在一个关键的技术拐点。当ChatGPT月活突破亿级、Gemini Pro实现多模态实时交互、Claude 3.5 Sonnet突破百万上下文长度&#xff0c;这些里程碑事件背后&#xff0c;一个崭新的大门逐步打开&#…

STM32F407VET6学习笔记9:编译输出固定大小.bin文件

今日学习如何输出固定大小的.bin编译文件 目录 Keil_V5 fromelf.exe 软件目录&#xff1a; 魔棒添加命令输出bin文件&#xff1a; 输出固定大小的bin文件&#xff1a; 计算bin文件大小&#xff1a; 安装 SRecord 工具集&#xff1a; 使用SRecord&#xff1a; 参考文章&#…

Spring Cloud 学习 —— 简单了解

Spring Cloud 简介 官方文档&#xff1a;https://docs.spring.io/spring-cloud-release/reference/index.html 在学习 Spring Cloud 之前&#xff0c;先了解一下什么是分布式系统&#xff1f; 分布式系统 分布式系统是由多个独立计算机&#xff08;节点&#xff09;通过网络…

FreeRTOS多任务系统①

多任务系统 回想一下我们以前在使用 51、STM32 单片机裸机(未使用实时操作系统)的时候一般都是在main 函数里面用 while(1)做一个大循环来完成所有的处理&#xff0c; 即应用程序是一个无限的循环&#xff0c; 循环中调用相应的函数完成所需的处理。 有时候我们也需要中断中完…

Celery简介

一、什么是异步任务队列 异步任务队列是指一种用于管理和调度异步执行任务的机制。具体来说&#xff0c;它允许将任务放入队列中&#xff0c;然后由后台进程异步处理这些任务&#xff0c;而不会阻塞主线程的执行。这种设计使得系统能够高效地处理耗时操作&#xff0c;同时保持…

【Livox雷达使用】

记录 目前livox雷达型号较多&#xff0c;适用范围广泛。后来出的雷达需要使用使用第二代SDK和驱动&#xff0c;如Mid360、HAP。之前在github上看有人问是否能一起安装&#xff0c;官方回答是可以的&#xff0c;我把livox SDK、livox_ros_driver和SDK2、driver2都下载了进行比较…

RS232转Profinet网关在检漏仪与西门子PLC里的应用

RS232转Profinet网关在检漏仪与西门子PLC里的应用 在工业自动化和控制领域&#xff0c;设备间的高效通信至关重要。RS232转Profinet网关作为一种关键的转换工具&#xff0c;能够将传统的RS232接口设备接入现代化的Profinet网络&#xff0c;从而实现数据的无缝传输和设备的远程…

公链地址生成曲线和算法

在区块链公链中&#xff0c;除了 ECDSA&#xff08;基于 secp256k1 曲线&#xff09; 和 EdDSA&#xff08;基于 Ed25519 曲线&#xff09; 之外&#xff0c;还有其他一些加密算法和椭圆曲线被用于生成公私钥对、签名验证或地址生成。这些算法和曲线的选择通常基于安全性、性能…

⭐ Unity AVProVideo插件自带播放器 脚本重构 实现视频激活重置功能

一、功能概述 本笔记记录直接修改插件自带的场景播放其中 原始的 MediaPlayerUI 脚本,实现激活时自动重置播放器的功能。 我用的插件版本是 AVPro Video - Ultra Edition 2.7.3 修改后的脚本将具备以下特性: 激活 GameObject 时自动重置播放位置到开头 可配置是否在重置后自…

C#命名类型前缀习惯改进

我这几天有一个疑惑&#xff0c;我之前用过一些变量命名&#xff0c;有些混乱&#xff0c;如string sql&#xff0c;string strSql&#xff0c;string sqlStr&#xff0c; string strName&#xff0c;string nameStr&#xff0c;bool boValid&#xff0c;stringbuilder sbFileN…

生成式AI如何重塑设计思维与品牌创新?从工具到认知革命的跃迁

当MidJourney生成的视觉方案出现在国际设计奖项的决赛名单&#xff0c;当Adobe Firefly成为设计师的标配工具&#xff0c;一个问题正从行业边缘走向中心&#xff1a;生成式人工智能&#xff08;GAI&#xff09;究竟在解构还是重构创意领域&#xff1f;作为深度参与AI与设计融合…

零知开源——STM32F407VET6驱动Flappy Bird游戏教程

简介 本教程使用STM32F407VET6零知增强板驱动3.5寸TFT触摸屏实现经典Flappy Bird游戏。通过触摸屏控制小鸟跳跃&#xff0c;躲避障碍物柱体&#xff0c;挑战最高分。项目涉及STM32底层驱动、图形库移植、触摸控制和游戏逻辑设计。 目录 简介 一、硬件准备 二、软件架构 三、…

超高频RFID读写器天线分类及应用场景

超高频RFID(Radio Frequency Identification,射频识别)技术作为一种先进的自动识别技术,已经在多个领域得到了广泛应用。作为RFID系统的重要组成部分,超高频RFID读写器天线不仅影响着系统的读取距离、读取速度和准确性,还决定了RFID系统的适应性和灵活性。本文将详细介绍…

第J2周:ResNet50V2算法实战与解析

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 batch_size32&#xff1a;每次训练取32张图像组成一个 batch img_size(224, 224)&#xff1a;图像输入大小匹配 ResNet50 的输入要求 epochs10&#xff1a;训练…

界面控件DevExpress WinForms中文教程:Banded Grid View - 如何固定Bands?

DevExpress WinForms拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForms能完美构建流畅、美观且易于使用的应用程序&#xff0c;无论是Office风格的界面&#xff0c;还是分析处理大批量的业务数据&#xff0c;它都能轻松胜…

安全帽检测

通过百度网盘分享的文件&#xff1a;工地项目 链接&#xff1a;https://pan.baidu.com/s/1pVxriAKKodwrcf_4Ou-OZg?pwdn2rv 提取码&#xff1a;n2rv --来自百度网盘超级会员V2的分享 YOLOv5训练自定义模型 YOLOv5需要安装pytorch、cuda、cudnn&#xff0c;可以参考我的环…

晨控CK-UR08与欧姆龙PLC配置Ethernet/IP通讯连接操作手册

晨控CK-UR08与欧姆龙PLC配置Ethernet/IP通讯连接操作手册 晨控CK-UR08系列作为晨控智能工业级别RFID读写器,支持大部分工业协议如RS232、RS485、以太网。支持工业协议Modbus RTU、Modbus TCP、Profinet、EtherNet/lP、EtherCat以及自由协议TCP/IP等。 本期主题&#xff1a;围绕…

windows无法安装到这个磁盘,选中的磁盘采用gpt分区仪式

解决办法&#xff1a; 我才用的是一个网友分享的微软官方解决办法&#xff0c;成功了&#xff0c;但是不知道会不会i有什么影响。将所有分区删掉&#xff0c;这时磁盘变成为分配的空间。我个人是两块固态&#xff0c;一块m.2&#xff0c;一块sata&#xff1b;所以我直接将500g…

JVM内存模型

JVM内存模型 说明&#xff1a; 1、JVM由装载子系统、运行时数据区&#xff08;jvm内存模型&#xff09;、字节码执行引擎&#xff1b; 2、运行时数据区包含堆、元空间、栈、本地方法栈和程序计数器&#xff1b; 3、堆、元空间是线程共享&#xff1b;方法栈、程序计数器是线程…