【仿muduo库实现并发服务器】实现时间轮定时器
实现时间轮定时器
- 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对象。
- 首先第一个将任务封装起来,让这个任务呢在一个对象析构的
时候再去执行它。
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);}
}
相关文章:

【仿muduo库实现并发服务器】实现时间轮定时器
实现时间轮定时器 1.时间轮定时器原理2.项目中实现目的3.实现功能3.1构造定时任务类3.2构造时间轮定时器每秒钟往后移动添加定时任务刷新定时任务取消定时任务 4.完整代码 1.时间轮定时器原理 时间轮定时器的原理类似于时钟,比如现在12点,定一个3点的闹…...
Conda更换镜像源教程:加速Python包下载
Conda更换镜像源教程:加速Python包下载 为什么要更换conda镜像源? Conda作为Python的包管理和环境管理工具,默认使用的是国外镜像源,在国内下载速度往往较慢。通过更换为国内镜像源,可以显著提高包下载速度ÿ…...
蓝桥杯 盗墓分赃2
原题目链接 问题描述 在一个探险者的团队中,小明和小红是合作的盗墓贼。 他们成功盗取了一座古墓中的宝藏,包括 n 件不同重量的珍贵文物和黄金,第 i 件宝藏的重量为 ai。 现在,他们希望公平地分配这些宝藏,使得小明…...
深度解读 Qwen3 大语言模型的关键技术
一、模型架构设计 Qwen3 延续了当前主流大型语言模型的 Transformer 架构,并在此基础上进行了多项增强设计,包含特殊的 Transformer 变体、位置编码机制改进、混合专家 (MoE) 技术引入,以及支持多模态和双重思考模式的新特性。 1. Transformer 基础架构与增强 基础架构:…...
使用 mysqldump 获取 MySQL 表的完整创建 DDL
要获取 MySQL 中某个表的完整创建 DDL(仅结构,不含数据),可以使用 mysqldump 工具的以下命令: 基本命令格式 bash mysqldump -h [主机名] -u [用户名] -p --no-data --single-transaction --routines --triggers --…...

day15 leetcode-hot100-28(链表7)
2. 两数相加 - 力扣(LeetCode) 1.模拟 思路 最核心的一点就是将两个链表模拟为等长,不足的假设为0; (1)设置一个新链表newl来代表相加结果。 (2)链表1与链表2相加,具…...
阿里云云效对接SDK获取流水线制品
参考文档: API旧版 企业令牌 https://help.aliyun.com/zh/yunxiao/developer-reference/api-reference API新版 个人令牌 https://help.aliyun.com/zh/yunxiao/developer-reference/api-reference-standard-proprietary API 个人令牌 https://www.alibabacloud.com…...
Qt 相关 编译流程及交叉编译 部署所遇到的问题总结-持续更新
准备环境和工具 1、主机环境 ubuntu20 2、交叉编译器 gcc-linaro-6.3.1…arm-linux-gnuebihf 3、QT5源码包qt-5.11.3_sources 下载qt-5.11.3的包,自己想办法下载 网盘啥的 都ok,再访问下载目录就可以显示了。 Index of /archive/qt 4、依赖库安装 sudo …...
前端面经 DNSxieyi1
域名解析协议 域名转为目标IP地址 两种方式 1 递归查询 A请求B B一定会告诉IP 2迭代查询 A请求B 如果B无能 ,B会告诉A如何获得改内容,但是B自己不会发出请求1 步骤 1.检查浏览器DNS 2.没有命中继续查询操作系统的DNS缓存 3.查询本地域名服务器&…...
如何通过ES实现SQL风格的查询?
一、Spring项目集成方案 添加依赖(pom.xml): <dependency><groupId>co.elastic.clients</groupId><artifactId>elasticsearch-java</artifactId><version>8.12.0</version> </dependency> <dependency><…...

知识图谱:重构认知的智能革命
在数字经济的浪潮中,知识图谱正悄然掀起一场认知革命。它不仅是技术的迭代,更是人类从“数据依赖”迈向“知识驱动”的里程碑。当谷歌用知识图谱优化搜索引擎、银行用它穿透复杂的金融欺诈网络、医院用它辅助癌症诊疗时,这项技术已悄然渗透到…...
【计算机网络】4网络层①
这篇笔记讲IPv4和IPv6。 为了解决“IP地址耗尽”问题,有三种措施: ①CIDR(延长IPv4使用寿命) ②NAT(延长IPv4使用寿命) ③IPv6(从根本上解决IP地址耗尽问题) IPv6 在考研中考查频率较低,但需掌握基础概念以防冷门考点,重点结合数据报格式和与 IPv4 的对比记忆。…...

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

Dropout 在大语言模型中的应用:以 GPT 和 BERT 为例
引言 大型语言模型(LLMs)如 GPT(生成式预训练 Transformer)和 BERT(双向编码器表示 Transformer)通过其强大的语言理解和生成能力,彻底改变了自然语言处理(NLP)领域。然…...
CentOS 7 如何安装libsndfile?
CentOS 7 如何安装libsndfile? # 配置编译环境 yum install -y gcc gcc-c make# 下载libsndfile压缩软件包 wget http://www.mega-nerd.com/libsndfile/files/libsndfile-1.0.25.tar.gztar -xf libsndfile-1.0.25.tar.gz cd libsndfile-1.0.25./configure --prefix/home/libs…...
基于深度学习的语音识别系统设计与实现
以下是为您准备的《基于深度学习的语音识别系统》技术文档,内容包含完整实现方案和详细代码解析: 基于深度学习的语音识别系统设计与实现 目录 语音识别技术概述系统架构设计语音信号预处理深度神经网络模型构建端到端语音识别实现模型训练与优化策略部署与性能优化完整代码…...

gitLab 切换中文模式
点击【头像】--选择settings 选择【language】,选择中文,点击【保存】即可。...

133.在 Vue3 中使用 OpenLayers 实现画多边形、任意编辑、遮罩与剪切处理功能
🎬 效果演示截图(先睹为快) ✨ 功能概览: ✅ 鼠标画任意形状多边形; ✏️ 点击“修改边界”可拖动顶点; 🟥 点击“遮罩”后地图除多边形区域外变红; ✂️ 点击“剪切”后仅显示选…...

4.8.4 利用Spark SQL实现分组排行榜
在本次实战中,我们的目标是利用Spark SQL实现分组排行榜,特别是计算每个学生分数最高的前3个成绩。任务的原始数据由一组学生成绩组成,每个学生可能有多个成绩记录。我们首先将这些数据读入Spark DataFrame,然后按学生姓名分组&am…...
40. 自动化异步测试开发之编写异步业务函数、测试函数和测试类(类写法)
40. 自动化异步测试开发之编写异步业务函数、测试函数和测试类(类写法) 一、类结构设计解析 1.1 基类设计 class Base:async_driver None # 🚗 存储浏览器驱动实例async def get(self, url: str http://secure.smartbearsoftware.com/.…...

【五子棋在线对战】一.前置知识的了解
前置知识的了解 前言1.Websocketpp1.1 使用Websocketpp的原因1.2 Websocket常用接口1.3 Websocket搭建服务器流程 2.JsonCpp2.1 Json 数据对象类的表示2.2序列化和反序列化的接口2.3 演示代码 3.Mysql
历年中国科学技术大学计算机保研上机真题
2025中国科学技术大学计算机保研上机真题 2024中国科学技术大学计算机保研上机真题 2023中国科学技术大学计算机保研上机真题 在线测评链接:https://pgcode.cn/school?classification1 拆分数字 题目描述 给定一个数字,拆分成若干个数字之和ÿ…...
内联盒模型基本概念?——前端面试中的隐形考点剖析
导语 在前端开发中,盒模型是基础知识,但“内联盒模型”往往容易被忽视。它不是“能不能写出页面”的问题,而是“写出的页面为何错位、如何精准定位”的问题。很多面试官会借这个考点,判断候选人对浏览器渲染机制的理解是否深入。…...

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种: 1.关键字: color:green; gray red yellow 2.rgb表示法:红,绿,蓝三原色。rgb(r,g,b),r表示红色,g表示绿…...
const ‘不可变’到底是值不变还是地址不变
const的基础规则 声明时必须初始化 const a; // ❌ 报错:Missing initializer in const declaration const b 10; // ✅ 正确块级作用域(const 的作用域仅限于声明它的代码块) if (true) {const x 100; } console.log(x); // ❌ 报错…...

如何找到一条适合自己企业的发展之路?
一个创业型的企业,开始就需要面向市场,通过自己的服务或产品,帮助用户解决问题,为客户创造价值,通过为客户创造的价值,出创造一定的的现金流,让企业存活下来! 企业的运营过程中&…...

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 在全局范围内的行为,包括如何处理大文件、SSL 证书验证以及提交时的用户信息。 git config --global --list http.sslVerifyfalse 这个配置项禁用了 SSL 证书验证。这在与自签名证书的 Git 服…...

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