C++ 手写常见的任务定时器
序言
最近在编写 C++ 的服务器代码时,我遇到了一个需求,服务器很可能会遇到那些长期不活跃的连接,这些连接占用了一定的资源但是并没有进行有效的通信。为了优化资源使用,我决定实现一个定时器,以便定期检查连接的活跃状态并适时关闭那些不再活跃的连接。
在这里我将介绍以下三种方式:
1. 循环遍历任务定时器
1.1 原理
这个的原理就相当简单了,我们利用一块连续的空间来存储我们需要执行的任务以及指定执行的时刻,每隔一段时间来判断哪些任务就绪了,我们就执行该任务。具体流程图如下:
在这里有几个点需要注意了,需求不同,设计也不同:
- 执行后是否移除:当我们执行我们的定时任务后,是否需要将他从我们的任务队列移除呢,这个完全就取决于自己的需求了,就比如我的需求是关闭某个连接,那么执行该任务后连接也没了,自然是要移除该任务的
- 休眠时间:这个就要看你的要求是否严格了,如果需要较小的时间差异,那么就将休眠时间间隔短一些,但是 CPU 的负担也大。
1.2 实现
首先我们需要一块空间来存储我们的任务并且方便遍历,那么就首选 vector
了,我们还需要设计在容器中元素的类型,该类型的任务是表示一个定时任务:
using TaskFunc = std::function<void()>;
using Timer = std::chrono::steady_clock::time_point;struct TimerTask
{Timer _execute_time; // 定时器TaskFunc _call_back; // 回调函数
};
之后我们定时器无非就是添加任务,启动函数:
// 添加定时任务
void AddTask(TaskFunc callback, int timeout)
{auto execute_time = std::chrono::steady_clock::now() + std::chrono::seconds(timeout); // 计算执行时间_tasks.push_back({ execute_time, callback });
}// 运行定时器
void Loop()
{while (true) {auto now = std::chrono::steady_clock::now();for (auto it = _tasks.begin(); it != _tasks.end(); ++it){if (now >= it->_execute_time){it->_call_back();it = _tasks.erase(it);}}std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}
实现起来还是比较简单的,但是在需要注意,在执行的时候一般是使用一个专门的线程来进行定时器的运行,这可能会涉及到线程安全的问题,所以我们需要进一步完善的话,需要进行加锁操作。
1.3 优缺点
优点很明显:
- 简单易用:循环遍历定时器通常简单直观,易于实现和理解。
是的就没了,我也没想出其他的优点,欢迎大家在评论区提出。
缺点的话也很明显:
- 资源消耗:如果循环遍历频率过高,可能会导致 CPU 资源浪费,尤其是在高负载情况下。
- 效率问题:当定时器数量很大时,每次遍历所有定时器会变得低效,因为即使大部分定时器尚未超时,也需要被检查。
作为一个启蒙的任务定时器还不错的,但是实际使用的话还是算了吧…
2. 小堆任务定时器
1.1 原理
首先我们先回忆一下小堆这种数据结构:
在小堆中,树的每个节点的值都小于或等于其子节点的值。这意味着堆的最小元素总是位于树的根部,即堆顶。 当我们取出该堆的最小元素时,步骤是:
- 交换堆顶和末尾元素:将堆顶元素(最小元素)与堆的最后一个元素交换位置。此时,堆顶元素不再是最小元素
- 调整堆:从堆顶开始,将交换后的堆顶元素与它的子节点比较,如果它大于其子节点中的任何一个,则与较小的子节点交换位置,并继续这一过程直到堆顶元素小于其所有子节点或到达叶节点。
- 移除最小元素:由于最小元素已经与堆的最后一个元素交换,现在可以直接从数组中移除它
这和我们说的任务定时器啥关系呢?现在,我们堆的每一个元素就不再是一个数了,而是我们的 TimerTask
,我们将所有任务构造为一个小堆,之后任务检测步骤如下:
- 检查堆顶元素:检查堆顶的任务(时间最小的任务)执行时间是否到了
-
- 条件不满足: 直接休眠指定时间再检查,时间最小的都不行,其他的更不行了
- 条件满足:取下堆顶元素执行任务,调整堆的结构使其为小堆
- 重复 1 - 2 的步骤
这样的话我们就不需要每次都遍历整个数组,具体的优缺后面说。
1.2 实现
在这里我们存储人物的容器就换成 priority_queue
了,因为需要实现一个小堆。其次任务结构体,我们需要在原来的基础上加上一个比较函数,因为涉及到自定义结构体排序比较的操作:
struct TimerTask
{TimerTask(Timer time, TaskFunc callback): _execute_time(time), _call_back(callback){}// 比较函数但是底层我们写为 > , 因为我们需要得到一个小堆bool operator<(const TimerTask& other) const {return _execute_time > other._execute_time;}Timer _execute_time; // 定时器TaskFunc _call_back; // 回调函数
};
之后稍有一些不一样的地方就是启动函数:
void Loop()
{while (true) {auto now = std::chrono::steady_clock::now();while (! _tasks.empty() && _tasks.top()._execute_time <= now) {auto task = _tasks.top();_tasks.pop();task._call_back();}std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}
实现起来不难,主要是思想很重要。所以说熟悉数据结构是很重要一件事!
1.3 优缺点
网上都说小堆的性能是好于我们第一种遍历方案的,但是我们要思考高效在哪里呢?我们算一笔账,在 n 个任务中 m 个任务就绪了, 前者执行定时任务的时间复杂度是 O(n)
,因为是遍历嘛;后者的话涉及到堆的调整操作 O(logn)
,一共取 m,那就是 O(mlogn)
。
当我们的数据量也就是 n 较小的情况下,请问谁更好一点呢?我想,是前者吧。(化学上有一句话是,抛开计量谈毒性,就是耍流氓!咋们计算机也差不多,抛开数据量谈高效,也是耍流氓!)
优点:
- 高效的任务管理:当数据量较大时,性能更加的不错。
- 底部开销:不和 vector 一样,不需要频繁的扩容操作
缺点:
- 不适合小任务集:当任务数量很少时,使用小堆可能反而不如简单的线性结构(如数组或链表)高效,因为堆的维护开销可能超过直接遍历的成本。
所以我们还是要带着辩证的角度看待问题!
3. 时间轮定时器
1.1 原理
大家对秒钟肯定都比较的熟悉吧,秒钟的单位是 1s,每隔一秒就走一步。现在,我们也创建一个类似于秒钟的功能的数据结构:
一共有八个格数,如果我们依旧使用秒为单位的话,那么最多可以表示 8s。这和我们今天的任务定时器的关系是什么呢?我们将该计时任务存储在现在 (秒针 + 任务时间)% 最大刻度
的位置上,并且秒针指向哪个位置,哪个位置的任务就执行!
没听懂,没关系,我第一次也没听懂,那我们模拟一遍,相信会好很多:
- 不越界的情况:现在秒针指向位置 1,我想要添加一个时间为 2s 的任务,那么该该任务存储在
(1 + 2)% 8 = 3
的位置。时间过 1s ,秒针指向 2,执行 2 中的全部任务(现在为空,就不执行)!时间再过 1s,秒针指向 3,执行 3 中的全部任务。OK!我们的目标达成了! - 越界的情况:现在秒针指向位置 3,我想要添加一个时间为 7s 的任务,那么该该任务存储在
(3 + 7)% 8 = 2
的位置。情况就和第一种一样了,所以%运算
保证了我们的任务存储在恰当的运算!
这就是时间轮!现在基本怎么运行大家心里或多或少了解了一点,我提出一些疑问,也可能会是你的疑问:
- 如果我有多个定时任务需要存储在 2 的位置,怎么办?
- 你的刻度最大 8s,我想要一个 10s 的任务,怎么办?
问题一:
在我们的哈希表中如果产生了哈希冲突,我们采用的一个方式为 链地址法。(新增的话,使用链表更为高效,不用进行扩容操作)我们也可以呀:
问题二:
所以我们在创建时间轮时我们需要确定一个合适的时间范围大小。但是如果我想要一个 1000000s 大小的时间轮不可能申请这么大个空间吧?当然不是,大家可以去了解一下多级时间轮。
1.2 实现
首先我们需要考虑什么数据结构是环状的呀,好像并没有接触过。其实我们使用 vector 就可以啦!啊?后者不是线性的一段空间吗,怎么会是环状的呢?没关系,我们再遍历数组的时候使用 %运算
不就好了嘛。我演示一下:
int main()
{std::vector<int> array = { 1,2,3,4,5 };int index = 0;while (true){std::cout << array[index] << " ";index = (index + 1) % array.size(); // 这里是关键,每当遍历完时,重头再来std::this_thread::sleep_for(std::chrono::seconds(1));}return 0;
}
输出结果:
1 2 3 4 5 1 2 3 4 5 1 …
所以说经过 %
他的逻辑结构已经成环了!
之后我们需要了解时间轮的成员变量应该需要哪些:
using TaskFunc = std::function<void()>;
using Wheel = std::vector<std::list<TaskFunc>>;int _tick; // 秒针
int _capacity; // 最大容量(刻度)
Wheel _wheel; // 时间轮
有了这些之后,我们需要指定构建函数,构建函数中需要指定时间轮的最大刻度:
TimeWheel(int capacity) : _tick(0), _capacity(capacity), _wheel(_capacity)
{}
最后也就是我们的新增函数以及启动函数:
void AddTask(TaskFunc callback, int timeout)
{int index = (_tick + timeout) % _capacity;_wheel[index].push_back(callback);
}void Loop()
{while (true) {std::this_thread::sleep_for(std::chrono::seconds(1));_tick = (_tick + 1) % _capacity; // 秒针移动// 执行该位置的函数for (auto& func : _wheel[_tick]){func();}// 清除_wheel[_tick].clear();}
}
在这里我们尤其需要注意不要越界了!
1.3 优缺点
优点:
- 高效性:时间轮能够以
O(1)
的复杂度处理定时任务的插入和删除,适合大量定时任务的场景。 - 节省空间:通过使用固定大小的轮子和槽,时间轮在内存使用上较为高效,避免了过多的动态内存分配。
缺点:
- 时间精度限制:时间轮的精度由槽的大小决定,若需要高精度的定时任务,可能不适合使用时间轮。
到现在为止,时间轮是我见过最精妙的一种方式,编程之美呀!
4. 总结
今天的内容简单地向大家介绍了三种任务定时器方式,遍历方式最为简单,效率也比较低,小堆方式处理大数据集有优势,数据量少了消耗反而还多了,最后一种时间轮是比较全面的一种,但是要确定合适的时间范围。希望大家有所收获!
相关文章:

C++ 手写常见的任务定时器
序言 最近在编写 C 的服务器代码时,我遇到了一个需求,服务器很可能会遇到那些长期不活跃的连接,这些连接占用了一定的资源但是并没有进行有效的通信。为了优化资源使用,我决定实现一个定时器,以便定期检查连接的活跃状…...

【VS+QT】联合开发踩坑记录
最新更新日期:2024/11/05 0. 写在前面 因为目前在做自动化产线集成软件开发相关的工作,需要用到QT,所以选择了VS联合开发,方便调试。学习QT的过程中也踩了很多坑,在此记录一下,提供给各位参考。 1. 环境配…...

PH热榜 | 2024-11-05
DevNow 是一个精简的开源技术博客项目模版,支持 Vercel 一键部署,支持评论、搜索等功能,欢迎大家体验。 Github:https://github.com/LaughingZhu/DevNow 1. FullContext 标语:用自然语言,让你的市场推广流…...

模拟机器人逐字回答,类似于实时回话
代码如下 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </head><…...

Java学习路线:JUL日志系统(一)日志框架介绍
目录 打印日志 日志的级别 打印文件 日志过滤器 日志输出流程 首先,为什么要使用日志系统? 如果单纯地用System.out.println打印信息,如果项目比较大,存在大量的信息就会显得非常凌乱。 而且,当我们希望在debug的…...

[渲染层网络层错误] net::ERR_CONTENT_LENGTH_MISMATCH 问题解决
问题描述 问题背景 微信小程序访问后端img资源的时候,偶尔出现这个感叹号,图片加载不出来,但是对应的url贴出来在浏览器中访问,或者重新加载是可以访问的。 错误描述 经查询前端报错 [渲染层网络层错误] net::ERR_CONTENT_LE…...

C 语言编程中的常见错误及解决方案
在 C 语言开发中,编译和链接错误是常见的问题,尤其是在处理多个源文件时。本文将总结一些常见的错误,并提供相应的解决方案,以帮助开发者更高效地排查和修复这些问题。 1. 结构体作用域问题 问题描述 在函数参数列表中定义结构体…...

开源模型应用落地-glm模型小试-glm-4-9b-chat-批量推理(二)
一、前言 GLM-4是智谱AI团队于2024年1月16日发布的基座大模型,旨在自动理解和规划用户的复杂指令,并能调用网页浏览器。其功能包括数据分析、图表创建、PPT生成等,支持128K的上下文窗口,使其在长文本处理和精度召回方面表现优异&a…...

【C++篇】数据之林:解读二叉搜索树的优雅结构与运算哲学
文章目录 二叉搜索树详解:基础与基本操作前言第一章:二叉搜索树的概念1.1 二叉搜索树的定义1.1.1 为什么使用二叉搜索树? 第二章:二叉搜索树的性能分析2.1 最佳与最差情况2.1.1 最佳情况2.1.2 最差情况 2.2 平衡树的优势 第三章&a…...

C#-类:声明类、声明类对象
一:类的声明 class 类名 {//特征——成员变量//行为——成员方法//保护特征——成员属性//构造函数和析构函数//索引器//运算符重载//静态成员 }类名:帕斯卡 同一个语句块中的不同类 不能重名 二:声明类对象 2.1 类的声明 ≠ 类对象的声…...

【AIGC】ChatGPT提示词Prompt高效编写技巧:逆向拆解OpenAI官方提示词
博客主页: [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 💯前言💯OpenAI官方提示词的介绍OpenAI官方提示词的结构与组成如何通过分析提示词找到其核心组件 💯OpenAI官方提示词分析案例一:制定教学计划案例二&…...

【linux】端口监听和终止进程
端口监听和终止进程 有时候,即使进程看起来已经关闭,它可能仍然占用着端口。你可以使用 netstat -tulpn | grep <端口号> 来查看哪个进程正在使用该端口,然后使用 kill -9 来强制关闭该进程。 [naienotebook-npu-b1bb152e-7655cb9d4…...

【网络安全】|kali中安装nessus
1、使用 df -h 命令查看磁盘使用情况,确保磁盘容量大于40G 简单粗暴办法:重装系统,装系统中注意磁盘空间相关的选项 //磁盘扩容:https://wiki.bafangwy.com/doc/670/ 2、安装 nessus 安装教程 https://blog.csdn.net/Cairo_A/a…...

Docker可视化管理面板DPanel的安装
本文软件由网友 rui 推荐; 什么是 DPanel ? DPanel 是一款 Docker 可视化管理面板,旨在简化 Docker 容器、镜像和文件的管理。它提供了一系列功能,使用户能够更轻松地管理和部署 Docker 环境。 软件特点: 可视化管理&…...

【android12】【AHandler】【3.AHandler原理篇AHandler类方法全解】
AHandler系列 【android12】【AHandler】【1.AHandler异步无回复消息原理篇】-CSDN博客 【android12】【AHandler】【2.AHandler异步回复消息原理篇】-CSDN博客 其他系列 本人系列文章-CSDN博客 1.简介 前面两篇我们主要介绍了有回复和无回复的消息的使用方法和源码解析&a…...

使用Docker Compose构建多容器应用
💓 博客主页:瑕疵的CSDN主页 📝 Gitee主页:瑕疵的gitee主页 ⏩ 文章专栏:《热点资讯》 使用Docker Compose构建多容器应用 引言 Docker Compose 简介 安装 Docker Compose 创建基本配置 运行多容器应用 查看服务状态 …...

面试知识目录
面试知识目录 八股文 java基础 java反射java HashMap面向对象多线程虚拟机内存 SpringMybatisMySQLPostgresqlSQL优化Nosql...

Rust移动开发:Rust在Android端集成使用介绍
Andorid调用Rust 目前Rust在移动端上的应用,一般作为应用sdk的提供,供各端使用,目前飞书底层使用Rust编写通用组件。 该篇适合对Android、Rust了解,想看如何做整合,如果想要工程源码,可以评论或留言有解疑…...

vue3动态监听div高度案例
案例场景 场景描述:现在左边的线条长度需要根据右边盒子的高度进行动态变化 实践代码案例 HTML部分 <div v-for"(device, index) in devices" :key"index"><!-- 动态设置 .left-bar 的高度 --><div class"left-bar"…...

数据转换 | Matlab基于SP符号递归图(Symbolic recurrence plots)一维数据转二维图像方法
目录 基本介绍程序设计参考资料获取方式 基本介绍 Matlab基于SP符号递归图(Symbolic recurrence plots)一维数据转二维图像方法 符号递归图(Symbolic recurrence plots)是一种一维时间序列转图像的技术,可用于平稳和非平稳数据集;对噪声具有…...

分类算法——逻辑回归 详解
逻辑回归(Logistic Regression)是一种广泛使用的分类算法,特别适用于二分类问题。尽管名字中有“回归”二字,逻辑回归实际上是一种分类方法。下面将从底层原理、数学模型、优化方法以及源代码层面详细解析逻辑回归。 1. 基本原理 …...

只允许指定ip远程连接ssh
我们都会使用securtcrt或者xshell等软件进行远程登录,这样虽然会给我们带来很多便捷,但是同样会存在一定的风险。有很多人专门通过重复的扫描试图破解我们的linux服务器,从而获取免费的“肉鸡”。因此我们可以通过设置hosts.allow和hosts.den…...

Rust 力扣 - 2841. 几乎唯一子数组的最大和
文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 我们遍历长度为k的窗口,用一个哈希表记录窗口内的所有元素(用来对窗口内元素去重),我们取哈希表中元素数量大于等于m的窗口总和的最大值 题解代码 use std::coll…...

TwinCL: A Twin Graph Contrastive Learning Model for Collaborative Filtering
TwinCL: A Twin Graph Contrastive Learning Model for Collaborative Filtering 摘要 在推荐和协同过滤领域,图对比学习(Graph Contrasive Learning,GCL)已经成为一种有影响的方法。然而,对比学习有效性的原因还没有…...

如何区分实例化网格中的每个实例
1)如何区分实例化网格中的每个实例 2)项目在模拟器上切换程序后有概率画面冻结 3)Unity工程导入团结引擎,GUID会变化,导致引用关系丢失 4)Mask在Android平台下渲染异常 这是第407篇UWA技术知识分享的推送&a…...

理解 WordPress | 第一篇:与内容管理系统的关系
初步了解 WordPress 在互联网世界里,WordPress 是一个家喻户晓的名字。它是一个开源的内容管理系统(Content Management System,简称 CMS),帮助用户轻松创建和管理网站。WordPress 诞生于 2003 年,最初是一…...

Python游戏脚本之实现飞机大战(附源码)
一.游戏设定 游戏界面如下图所示: 游戏的基本设定: 敌方共有大中小3款飞机,分为高中低三种速度; 子弹的射程并非全屏,而大概是屏幕长度的80%; 消灭小飞机需要1发子弹,中飞机需要8发,大飞机需要20发子弹; 每消灭一架小飞机得1000分,中飞机6000分,大飞…...

使用Spring Boot搭建简单的web服务
1 引言 1.1 Spring Boot简介 Spring Boot是由Pivotal团队提供的一套开源框架,旨在简化Spring应用的创建及部署。 一、核心设计思想 Spring Boot的核心设计思想是“约定优于配置”(Convention Over Configuration,简称COC)。这…...

【IF-MMIN】利用模态不变性特征进行缺失模态的鲁棒多模态情感识别
代码地址:github地址传送 文章是基于MMIN的改进 -> MMIN传送 abstract 多模态情感识别利用跨模态的互补信息来获得性能。然而,我们不能保证所有模式的数据总是存在于实践中。在跨模态数据缺失预测研究中,异质性模态之间的固有差异即模态…...

RGB图像,排列方式NHWC适合CPU计算,NCHW适合GPU计算
之前写过笔记OpenCV读取图像时按照BGR的顺序HWC排列,PyTorch按照RGB的顺序CHW排列,HWC格式排列,那么内存位置计算公式是? 在比较NHWC(channels_last)和NCHW(channels_first)这两种图像数据通道格式的效率时…...