使用一个定时器(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_xxxepoll 实现定时器的功能的主要逻辑如下面的流程图所示,一图胜千言,不再过多的文字解释。

在这里插入图片描述

下面介绍一些实现上的细节。

选用什么样的数据结构管理定时事件?对于定时事件的添加、删除和查找,要高效。因为只使用一个 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 函数。留作为后续改进。

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

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

相关文章

QEMU源码全解析41 —— Machine(11)

接前一篇文章&#xff1a;QEMU源码全解析40 —— Machine&#xff08;10&#xff09; 本文内容参考&#xff1a; 《趣谈Linux操作系统》 —— 刘超&#xff0c;极客时间 《QEMU/KVM》源码解析与应用 —— 李强&#xff0c;机械工业出版社 特此致谢&#xff01; 时间过去了几…

go语言(二十一)---- channel的关闭

channel不像文件一样需要经常去关闭&#xff0c;只有当你确实没有任何发送数据了&#xff0c;或者你想显示的结束range循环之类的&#xff0c;才去关闭channel。关闭channel后&#xff0c;无法向channel再发送数据&#xff0c;&#xff08;引发pannic错误后&#xff0c;导致接收…

Linux编译实时内核和打补丁

目录 1.Linux内核2.实时内核3.编译实时内核3.1 准备3.2 获取内核源码3.3 编译3.4 设置GRUB确保启动到实时内核 4.给内核打补丁5.安装新的内核 1.Linux内核 https://github.com/torvalds/linux Linux内核是Linux操作系统的核心部分&#xff0c;它是操作系统的基本组成部分&…

spring整合mybatis的底层原理

spring整合mybatis的底层原理 原理&#xff1a; FactoryBean的自定义对象jdk动态代理Mapper接口对象 一、手写一个spring集成mybatis 目录结构&#xff1a; 1.1 入口类 public class Test {public static void main(String[] args) {AnnotationConfigApplicationContext co…

Linux中的软链接与硬链接

Linux链接概念 Linux链接分两种&#xff0c;一种被称为硬链接&#xff08;Hard Link&#xff09;&#xff0c;另一种被称为符号链接&#xff08;Symbolic Link&#xff09;。默认情况下&#xff0c;使用 ln 命令不加参数创建硬链接&#xff0c;加 -s 参数则创建软链接 硬链接…

【mongoDB】创建用户账号和权限

使用use database_name 命令创建或切换到一个数据库 查看用户 show users 输入该命令后&#xff0c;无数据表示该数据库没有用户 创建用户 user:" freedom " 表示用户名为freedom pwd:" 123456 ” 表示密码为123456 roles:[" root "] …

一键去除图片背景——background-removal-js

一些JavaScript库和工具可以帮助实现背景去除&#xff1a; OpenCV.js&#xff1a;OpenCV的JavaScript版本&#xff0c;提供了许多计算机视觉功能&#xff0c;包括背景去除。Jimp&#xff1a;一个用于处理图像的JavaScript库&#xff0c;提供了许多图像处理功能&#xff0c;包括…

Deepin基本环境查看(六)【计算机、回收站、控制面板、文档、设备管理器】

Deepin基本环境查看 - 目录Deepin基本环境查看&#xff08;一&#xff09;【基本信息】Deepin基本环境查看&#xff08;二&#xff09;【内存】Deepin基本环境查看&#xff08;三&#xff09;【网络信息】Deepin基本环境查看&#xff08;四&#xff09;【硬盘/分区、文件系统、…

鸿蒙ArkUI 宫格+列表+HttpAPI实现

鸿蒙ArkUI学习实现一个轮播图、一个九宫格、一个图文列表。然后请求第三方HTTPAPI加载数据&#xff0c;使用了axios鸿蒙扩展库来实现第三方API数据加载并动态显示数据。 import {navigateTo } from ../common/Pageimport axios, {AxiosResponse } from ohos/axiosinterface IDa…

第5章 (python深度学习——波斯美女)

第5章 深度学习用于计算机视觉 本章包括以下内容&#xff1a; 理解卷积神经网络&#xff08;convnet&#xff09; 使用数据增强来降低过拟合 使用预训练的卷积神经网络进行特征提取 微调预训练的卷积神经网络 将卷积神经网络学到的内容及其如何做出分类决策可视化 本章将…

Android 性能优化总结:包体积优化

前言 随着开发不断迭代&#xff0c;App体积越来越大&#xff0c;包大小的增大也会给我们应用带来其他的影响 比如 下载率影响 过大的包体积会影响下载转化率&#xff0c;根据Google Play Store包体积和转化率分析报告显示&#xff0c;平均每增加1M&#xff0c;转化率下降0.2%左…

基于Java SSM框架实现大学生综合素质评分平台项目【项目源码+论文说明】

基于java的SSM框架实现大学生综合素质评分平台演示 摘要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所…

C#调用SqlSugar操作达梦数据库报错“无效的表或视图名”

安装达梦数据库后&#xff0c;使用SqlSugar连接测试数据库并基于DBFirst方式创建数据库表对应的类&#xff0c;主要代码如下&#xff1a; SqlSugarClient db new SqlSugarClient(new ConnectionConfig(){DbType DbType.Dm,ConnectionString "Serverlocalhost; User Id…

qemu单步调试arm64 linux kernel

一、背景和目的 qemu搭建arm64 linux kernel环境-CSDN博客 之前介绍了qemu启动kernel的配置步骤和方法&#xff0c;现在开始我们的调试&#xff0c;这篇文章主要讲解如何单步调试内核&#xff0c;所有的实验还是基于ARM64&#xff1b; 二、环境准备 需要准备hostx86 target…

容器化部署 Jenkins,并配置SSH远程操作服务器

目录 一、Jenkins是什么 二、常见的部署Jenkins的方法 三、为什么选择容器化部署 四、容器化部署Jenkins步骤 1、安装 Docker 2、获取 Jenkins 镜像 3、创建并运行容器 4、访问 Jenkins 4.1 查看初始密码问题 5、配置 Jenkins 5.1 安装插件 5.2 创建管理员用户 5.3…

Java项目实战--瑞吉外卖DAY03

目录 P22新增员工_编写全局异常处理器 P23新增员工_完善全局异常处理器并测试 p24新增员工_小结 P27员工分页查询_代码开发1 P28员工分页查询_代码开发2 P22新增员工_编写全局异常处理器 在COMMON新增全局异常捕获的类&#xff0c;其实就是代理我们这些controlle。通过aop把…

基于SSM+Shiro+Druid实现的企业资产后台管理系统

系统介绍 系统演示 关注【全栈小白】视频号查看演示视频 随着企业的发展&#xff0c;很多中小企业的规模越来越大&#xff0c;需要管理资产也越来越多&#xff0c;比如显示器&#xff0c;主机&#xff0c;打印机&#xff0c;传真机&#xff0c;复印机&#xff0c;电话&#…

SAP-PP: BOM选择标识不存在

在MM03 中选择生产版本无法打开 原因是未维护 BOM选择标识 配置路径&#xff1a;生产→物料需求计划→工厂参数→执行工厂参数总体维护→BOM/工艺路线选择 T-CD: OPPQ

flutter极光推送配置厂商通道(华为)笔记--进行中

一、基础集成按照下面官方文档进行 厂商通道相关参数申请教程 集成厂商 集成指南 官方文档&#xff1a;厂商通道回执配置指南 注意&#xff1a;不同厂商对app上架的要求不同&#xff0c;华为、荣耀 对app上架没有硬性要求 遇到问题 1、引入apply plugin: com.huawei.agconn…

Jenkins部署及应用

一. 简介 二. 下载地址源&#xff1a; …