使用一个定时器(timer_fd)管理多个定时事件
使用一个定时器(timer_fd)管理多个定时事件
使用 timerfd_xxx 系列函数可以很方便的与 select、poll、epoll 等IO复用函数相结合,实现基于事件的定时器功能。大体上有两种实现思路:
- 为每个定时事件创建一个 timer_fd,绑定对应的定时回调函数,然后将 timer_fd 注册到 epoll(或其它IO复用函数)中,当 timer_fd 可读,调用其回调函数,然后关闭该文件描述符。
- 只创建一个 timer_fd。管理所有定时事件,timer_fd 每次只关注时间序列上下一个将要超时的时间,当 timer_fd 变得可读,从管理的所有定时事件中查找比 timer_fd 可读时刻小的定时事件,然后执行对应的回调函数。
这两种方法中,第一种实现起来相对简单,但一个定时事件就对应一个文件描述符,当定时事件较少且创建周期不频繁时,该方法没啥问题;但当定时事件较多,且定时事件的创建和销毁频繁时,会导致文件描述符的频繁创建和关闭,影响服务器性能。第二种方法只使用一个 timer_fd 来管理所有定时事件,能避免文件描述符频繁创建和关闭带来的系统影响,但在实现上相对复杂,关键在于如何高效地管理所有还未超时的定时事件。
下面将具体介绍第二种方法的实现思路和一些实现上的细节,该思路主要来自 muduo 网络库的实现,我尝试对其进行了一点点改进,并将思考一并写在下文。
使用 timerfd_xxx 和 epoll 实现定时器的功能的主要逻辑如下面的流程图所示,一图胜千言,不再过多的文字解释。

下面介绍一些实现上的细节。
选用什么样的数据结构管理定时事件?对于定时事件的添加、删除和查找,要高效。因为只使用一个 timerfd 来管理多个定时事件,而 timerfd 每次只能关注一个超时时间,若每新添加一个定时事件,就调用 timerfd_settime 设置超时事件,会使前面的定时事件失效。因此一个自然的想法是,根据定时事件的超时时间从小到大排序,timerfd 每次只关注所有定时事件中超时时间最小的哪个时间。可以使用C++标准库中的 set 或 map 来管理定时事件,它们是有序集合,底层的数据结构为红黑树,插入、删除和查找的平均时间复杂度都为 O(log N),muduo 中就是使用 set 来管理定时事件的。
muduo 中的做法是,使用 set 来管理定时事件,set 中的元素类型为 pair<Timestamp,Timer*>。书中给出了采用这种做法的原因:
“不能直接用 map<Timestamp,Timer*>,因为这样无法处理两个Timer到期时间相同的情况。有两个解决方案,一是用 multimap 或 multiset,二是设法区分key。muduo现在采用的是第二种做法,这样可以避免使用不常见的 multimap class。具体来说,以 pair<Timestamp,Timer*> 为key,这样即便两个Timer的到期时间相同,它们的地址也必定不同。”
我在写定时器这部分功能时,采用的是 muduo 中提到的第二种方法,使用 multimap 管理定时事件。muduo 中 Timestamp 的精度为微妙,我的实现中 Timestamp 的精度为纳秒,因此几乎不可能存在两个 Timer 到期事件相同的情况,即便存在两个 Timer 到期的 Timestamp 相同,也无妨紧要,因为我在 Timer 类中添加了 TimerId 成员变量,用来唯一标识 Timer,可以通过成员函数来获取该标识。
如下TimerId、Timer和TimerQueue类的定义所示,TimerQueue中的 activeTimers_ 成员变量为 set 类型,其元素为 TimerId 类型,而 Timer 中具有获取 TimerId 成员变量的成员函数,因此在 TimerQueue 类中,timers_、activeTimers_ 和 cancelingTimers_ 三个成员变量可以很方便地进行相互转换查找。
class TimerId
{
public:friend class TimerQueue;public:TimerId(): id_(0), timer_(nullptr) {}TimerId(int64_t id, Timer* timer): id_(id), timer_(timer) {}// ...private:int64_t id_;Timer* timer_;
};class Timer
{
public:Timer(TimerCallback cb, Timestamp when, Seconds interval): callback_(std::move(cb)), expiration_(when), interval_(interval), repeat_(interval_ > 0.0),id_(++timerCount_, this){}// ...private:TimerCallback callback_;Timestamp expiration_;Seconds interval_; bool repeat_;const TimerId id_; // 定时器唯一标识static std::atomic_int64_t timerCount_;
};class TimerQueue
{
public:using TimerMap = std::multimap<Timestamp, std::unique_ptr<Timer>>;using TimerVector = std::vector<std::unique_ptr<Timer>>;using ActiveTimer = std::set<TimerId>;explicit TimerQueue(EventLoop *loop);// ...private:EventLoop *loop_;int timerfd_;Channel timerChannle_;TimerMap timers_;ActiveTimer activeTimers_;std::set<Timer*> cancelingTimers_;std::atomic_bool callingExpiredTimers_;
};
在确定了管理定时事件的数据结构后,按照上述所给的定时事件的处理流程来编写代码即可。在实现细节上,muduo 源码中对于上层应用删除一个定时器的实现,我觉得处理的很到位,在这里单独拿出来描述。
定时器对上层提供的接口无非就两类,一类是添加一个定时事件,另一类则是删除一个定时事件。添加一个定时事件的处理相比于删除一个定时事件要简单些,只需往 TimerMap 中插入一个 Timer 即可,然后判断一下添加的 Timer 的超时时间是否小于当前设置的超时时间,若小于则更新定时器的超时时间。这一步的实现很简单,因为 TimerMap 为 map 类型,只需 O(1) 的时间复杂度即可完成。
而对于删除一个定时事件,要复杂一些。
首先,对于定时事件超时,在处理完定时事件上的回调函数后,若超时的定时事件不是周期性的,需要能够自动删除。我通过 unique_ptr 来实现定时事件的自动删除,这也是 muduo 中提到的改进方法。当有定时事件发生,将超时的事件从 TimerMap move 到一个 vector 中,然后处理超时事件的回调函数,处理完后,若是周期性事件,则将其再次 move 到 TimerMap 中,若不是周期性事件,则什么都不用做,unique_ptr 管理的 Timer 会自动随着 vector 的析构而析构。
其次,对于上层调用删除指定实时事件,需要考虑这样一种场景,当具有周期性的定时事件超时后,且
正在处理定时事件的回调函数 时,上层应用调用删除指定定时事件的函数,且这个待删除的定时事件为正在超时处理的周期性定时事件。此时就不能简单的直接根据超时的定时事件为周期性事件就再次将其添加回 TimerMap 中,要同时判断其是否正在被删除。这也是为什么在 TimerQueue 类中需要具有一个 cancelingTimers_ 和 callingExpiredTimers_ 成员变量的原因,也是 muduo 中处理很巧妙的一部分。
具体的实现思路为:
- 使用 callingExpiredTimers_ 成员变量标识是否正在处理超时事件的回调函数。
callingExpiredTimers_ = true;cancelingTimers_.clear();// 处理超时事件的回调函数for (const auto& iter : expirationTimers) {iter->run();}callingExpiredTimers_ = false; - 在上层应用调用删除定时事件的函数时,若删除的定时事件已超时,且正在执行超时回调函数,则将其添加到 cancelingTimers_ 中。
// 若定时事件未超时,则可以直接从 TimerMap 中删除if (iter != activeTimers_.end()){// ...}// 若删除的定时事件已超时,且正在执行超时回调函数else if (callingExpiredTimers_) {cancelingTimers_.emplace(timer);} - 判断是否需要将超时的定时事件重新添加回 TimerMap 中。
// 超时的定时事件为周期性事件,且没有被删除,则重新添加回 TimerMap 中if (timer->repeat() && cancelingTimers_.find(t) == cancelingTimers_.end()) {timer->restart(when);insert(std::move(timer));}
我的实现采用了 muduo 中的实现方法,只不过我采用的是 multimap 来管理定时事件,在实现的细节上会有一些差别。此外,对于上述给出的 TimerQueue 类的定义,还可以有一些改进,将 activeTimers_ 和 cancelingTimers_ 成员变量改为 unordered_set 类型,查找删除的平均时间复杂度可以从 O(logN) 降到 O(1),不过需要自定义 hash 函数。留作为后续改进。
相关文章:
使用一个定时器(timer_fd)管理多个定时事件
使用一个定时器(timer_fd)管理多个定时事件 使用 timerfd_xxx 系列函数可以很方便的与 select、poll、epoll 等IO复用函数相结合,实现基于事件的定时器功能。大体上有两种实现思路: 为每个定时事件创建一个 timer_fd,绑定对应的定时回调函数…...
C++:使用tinyXML生成矢量图svg
先说一下tinyXML库的配置: 很简单,去下面官网下载 TinyXML download | SourceForge.net 解压后是这样 直接将红框中的几个文件放到项目中即可使用 关于svg文件,SVG是基于XML的可扩展矢量图形,svg是xml文件,但是xml…...
day34_js
今日内容 0 复习昨日 1 事件 1.1 事件介绍 1.2 事件绑定方式 1.3 不同事件的演示 2 DOM操作 2.1 概述 2.2 查找元素 2.3 元素内容的查找和设置 2.4 元素属性的查找和设置 2.5 元素CSS样式的查找和设置 2.6 创建元素 2.7 创建文本节点 2.8 追加元素 2.9 删除元素 3 案例练习 0 复…...
AR 自回归模型
文章目录 总的代码ADF 检验(是否平稳)差分操作拟合AR 模型预测可视化总的代码 import pandas as pd import numpy as np import matplotlib.pyplot as plt from statsmodels.tsa.ar_model import AutoReg from statsmodels.tsa.stattools import adfuller# 生成一个示例时间序…...
51单片机ESP8266
一、MQTT透传AT固件 安信可提供的烧录WiFi固件工具: 链接: https://docs.ai-thinker.com/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B72 安信可提供的固件库链接: https://docs.ai-thinker.com/%E5%9B%BA%E4%BB%B6%E6%B1%87%E6%80%BB 经过测试,选择这个不可以…...
php 源码加密保护 bease方案
推荐使用 php-bease 这个免费开源方案。 有一说一,这个项目上次更新时间是2021年… 多好的项目呀。 作者说在 php5.1 ~ php7.2 上都测试过。 源码地址: C源码: https://github.com/liexusong/php-beast dll版: https://github…...
FFMPEG解析ts流
三篇相关联的文章: ffmpeg下HLS解析过程-CSDN博客TS文件格式详解及解封装过程-CSDN博客 FFMPEG解析ts流-CSDN博客 一、简介 关于TS格式解析,可以参考《TS文件格式详解及解封装过程-CSDN博客》,本文主要代码部分解读。建议大家熟读iso138…...
Java基础-实现猜数字小游戏
1. 实现控制台的猜数字游戏。游戏运行时产生一个1~100之间的随机数字; 2. 要求用户从控制台输入数字,若输入的数字比随机数小,则提示太小了;若输入的数字比随机数大,则提示太大了,若输入的数字与随机数相同…...
爬虫(一)
1. HTTP协议与WEB开发 1. 什么是请求头请求体,响应头响应体 2. URL地址包括什么 3. get请求和post请求到底是什么 4. Content-Type是什么1.1 简介 HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于万维网(…...
【软件测试】学习笔记-Nginx 在系统架构中的作用
本篇文章你探讨 Nginx 在应用架构中的作用,并从性能测试角度看如何利用 Nginx 数据统计用户访问量。 Nginx 重要的两个概念 代理 首先要来解释一下什么是代理,正向代理和反向代理是什么意思?各自作用是什么?不少同学经常听到这…...
鸿蒙开发【应用开发基础知识】
应用开发介绍 1. 项目说明 通过OpenHarmony提供的Stage模型和ArkUI的eTS声明式开发规范,结合简单的Demo,分享学习OpenHarmony/docs/application-dev[应用开发文档] 2. 主要功能 目录标题展示,目录列表展示点击目录列表,查看列…...
腾讯云幻兽帕鲁4核16G14M服务器性能测评和价格
腾讯云幻兽帕鲁服务器4核16G14M配置,14M公网带宽,限制2500GB月流量,系统盘为220GB SSD盘,优惠价格66元1个月,277元3个月,支持4到8个玩家畅玩,地域可选择上海/北京/成都/南京/广州,腾…...
Linux第一个小程序——进度条
目录 回车和换行 缓冲区 设计倒计时 进度条(多文件操作) Version1:进度条 Version2:应用场景进度条 Version3:升级彩色进度条 回车和换行 回车\r:r 回车,回到当前行的行首,而…...
(N-141)基于springboot,vue网上拍卖平台
开发工具:IDEA 服务器:Tomcat9.0, jdk1.8 项目构建:maven 数据库:mysql5.7 系统分前后台,项目采用前后端分离 前端技术:vueelementUI 服务端技术:springbootmybatis-plusredi…...
深入了解Figure的结构与层次
深入了解Figure的结构与层次 一 Matplotlib中的Figure1.1 Figure的概念和作用:1.2.创建Figure对象:1.3 Figure的属性和方法: 二 子图(Axes)的角色与创建2.1 子图(Axes)的概念:2.2 创建子图的方法:2.3 Axes的…...
c语言基础6
1.逗号表达式 逗号表达式,就是用逗号隔开的多个表达式。 逗号表达式,从左向右依次执行。整个表达式的结果是最后⼀个表达式的结果。 我们来看下面的一个代码: int main() {int a 1;int b 2;int ret (a > b, a b 2, b, b a 1);p…...
kotlin sum 与 sumOf
kotlin 中 sum 的作用: 计算一个列表里面数字的总和: val numbers listOf(1, 2, 3, 4, 5) val sum numbers.sum() println("The sum is: $sum") // 打印结果: The sum is: 15 kotlin中sumOf的作用: 也是计算一个列表里面数字…...
php怎么输入一个变量,http常用的两种请求方式getpost(ctf基础)
php是网页脚本语言,网页一般支持两种提交变量的方式,即get和post get方式传参 直接在网页URL的后面写上【?a1027】,如果有多个参数则用&符号连接, 如【?a10&b27】 post方式传参 需要借助插件,ctfer必备插…...
Spring Boot 项目配置文件
文章目录 配置文件的作用properties基本语法读取文件信息缺点 yml基本语法优点配置不同数据类型字符串类型的写法 配置对象配置集合 读取配置文件的几种方法EnvironmentPropertySource使用原生方式读取 设置不同环境的配置文件 配置文件的作用 整个项目中重要的数据都是在配置…...
学校“数据结构”课程Project—扩展功能(自主设计)
目录 一、设想功能描述 想法缘起 目标功能 二、问题抽象 三、算法设计和优化 1. 易想的朴素搜索 / dp 搜索想法 动态规划(dp)想法 2. 思考与优化 四、算法实现 五、结果示例 附:使用的地图API 一、设想功能描述 想法缘起 OSM 导出…...
数模小白别慌!手把手教你用Python和MATLAB搞定国赛美赛(附2022年M奖/省一代码)
数模竞赛入门指南:从零到获奖的Python与MATLAB实战路径 数学建模竞赛对于初学者而言,往往像一座难以攀登的高山。第一次面对赛题时,那种无从下手的迷茫感我至今记忆犹新——三个队友围着一道看似简单的题目,却连该用什么工具、从哪…...
3步轻松让老旧Mac电脑升级最新macOS焕发新生
3步轻松让老旧Mac电脑升级最新macOS焕发新生 【免费下载链接】OpenCore-Legacy-Patcher 体验与之前一样的macOS 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 老旧Mac电脑升级最新macOS不再是难题!OpenCore Legacy Patcher是一…...
C++ 模板类型推导的底层实现
C模板类型推导的底层实现 C的模板类型推导是现代C编程中不可或缺的核心机制,它使得泛型编程变得灵活而高效。从简单的函数模板到复杂的元编程,类型推导在编译期间自动推断模板参数,减少了冗余代码。其底层实现机制却鲜为人知。本文将揭开模板…...
HY-Motion 1.0应用案例:为AR试衣间生成‘转身→抬手→比划’交互动作流
HY-Motion 1.0应用案例:为AR试衣间生成转身→抬手→比划交互动作流 1. 项目背景与需求 AR试衣间正在改变传统购物体验,但如何让虚拟服装在用户身上自然流动,一直是个技术难题。传统方案要么动作生硬不连贯,要么需要复杂的动作捕…...
Web 开发者零 AI 基础入门:Skill 开发实战全攻略
引言:提示词是即兴发挥,Skill 是专业标准前言:作为 Web 开发者,我们早已习惯「组件化开发、接口化调用、工程化部署」的工作流。面对 AI 应用落地,很多人误以为必须精通大模型、机器学习才能参与开发。事实上ÿ…...
YOLO X Layout中小企业应用:无需训练,开箱即用的文档结构理解AI工具
YOLO X Layout中小企业应用:无需训练,开箱即用的文档结构理解AI工具 1. 引言:让文档理解变得简单高效 在日常办公中,我们经常需要处理各种文档——扫描的合同、拍摄的表格、电子版报告。传统方式需要人工逐个识别文档中的文字、…...
高效命令行的OpenClaw搭配:nanobot镜像与zsh/fish集成
高效命令行的OpenClaw搭配:nanobot镜像与zsh/fish集成 1. 为什么需要命令行AI助手 作为一个长期与终端打交道的开发者,我发现自己每天要重复处理三类高频问题:记不清的命令参数、复杂的管道组合、报错信息的即时解读。传统解决方案要么依赖…...
DDrawCompat终极指南:让Windows 11完美运行经典DirectX老游戏
DDrawCompat终极指南:让Windows 11完美运行经典DirectX老游戏 【免费下载链接】DDrawCompat DirectDraw and Direct3D 1-7 compatibility, performance and visual enhancements for Windows Vista, 7, 8, 10 and 11 项目地址: https://gitcode.com/gh_mirrors/dd…...
OpenClaw低代码方案:Qwen3-32B将Excel需求转为自动化流程
OpenClaw低代码方案:Qwen3-32B将Excel需求转为自动化流程 1. 从Excel到ERP的自动化困境 上周市场部的同事又来找我帮忙了。他们每天要手动将几十份Excel表格里的客户订单录入到公司老旧的ERP系统里——这个上世纪风格的绿色界面软件,既没有批量导入功能…...
OpenClaw技能扩展指南:用QwQ-32B实现Markdown自动排版
OpenClaw技能扩展指南:用QwQ-32B实现Markdown自动排版 1. 为什么需要Markdown自动化技能 作为一个长期用Markdown写作的技术博主,我经常遇到这样的困扰:从不同来源收集的笔记格式混乱,手动调整标题层级、表格对齐和代码块语法要…...
