网易面试:手撕定时器

概述:

本文使用STL容器-set以及Linux提供的timerfd来实现定时器组件

所谓定时器就是管理大量定时任务,使其能按照超时时间有序地被执行

需求分析:

1.数据结构的选择:存储定时任务

2.驱动方式:如何选择一个任务并执行

一、数据结构的选择:红黑树

红黑树是一种查找、删除、插入时间复杂度为O(logN)的数据结构,性能均衡,STL的set和map就是基于红黑树实现的,与普通的红黑树不同,STL的红黑树设计添加了指向最大节点和最小节点的指针,这一点实现了set和map可以使用O(1)的时间复杂度查找最大值和最小值:

在这里插入图片描述

二、驱动方式:timerfd + epoll

Linux提供了定时机制timerfd,与sockfd一样,内核负责检测该文件描述符的就绪情况,并需要epoll等io多路复用机制向用户层通知

三、代码实现

1.定时任务节点:

struct TimerNodeBase { //  定时器节点基类,用于红黑树(set)存储time_t expire;     //  超时时间uint64_t id;       //  唯一 id, 用于解决超时时间相同的节点存储问题
};struct TimerNode : public TimerNodeBase {  // 子类定时器节点, 添加了一个回调函数using Callback = function<void(const TimerNode &node)>;Callback func;TimerNode(int64_t id, time_t expire, Callback func) : func(std::move(func)) { // 使用 move 右值引用,性能高this->expire = expire;this->id = id;}
};

这里设计两个结构体的目的是将回调函数和其它属性(超时时间和id)隔离开

2.运算符重载,用于比较两个节点的大小(超时时间大小)

bool operator < (const TimerNodeBase &lhd, const TimerNodeBase &rhd) { // 运算符重载,比较两个节点的大小// 先根据超时时间判定大小if (lhd.expire < rhd.expire) {return true;} else if (lhd.expire > rhd.expire) {return false;} // 超时时间相同时,根据 id 判断大小else return lhd.id < rhd.id;
}

3.定时器类:

class Timer {public:static inline time_t GetTick() { // 获取系统当前时间戳return chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now().time_since_epoch()).count();}TimerNodeBase AddTimer(int msec, TimerNode::Callback func) {time_t expire = GetTick() + msec; // msec是相对超时时间,expire是绝对超时时间(时间戳)// 如果待插入节点当前不是红黑树中最大的if (timeouts.empty() || expire <= timeouts.crbegin()->expire) { auto pairs = timeouts.emplace(GenID(), expire, std::move(func)); // emplace是在容器内部生成一个对象并插入到红黑树中,性能优于push的copy操作  2.使用move右值引用,避免copy// 使用static_cast将子类cast成基类return static_cast<TimerNodeBase>(*pairs.first); // emplace的返回值pair包含:1.创建并插入的节点 2.是否成功插入(已存在相同节点则插入失败)}// 如果待插入节点是最大的,直接插入到最右侧,时间复杂度 O(1) ,优化性能auto ele = timeouts.emplace_hint(timeouts.crbegin().base(), GenID(), expire, std::move(func));// 返回基类而不是子类return static_cast<TimerNodeBase>(*ele);}void DelTimer(TimerNodeBase &node) { // 从(set)红黑树中删除一个节点auto iter = timeouts.find(node); // 找到指定节点if (iter != timeouts.end())timeouts.erase(iter);       // 移除}void HandleTimer(time_t now) {     // 执行当前已超时的任务auto iter = timeouts.begin();while (iter != timeouts.end() && iter->expire <= now) {iter->func(*iter);iter = timeouts.erase(iter); // eraser返回下一个节点}}public:// 更新 timerfd 的到期时间为 timeouts 集合中最早到期的定时器时间virtual void UpdateTimerfd(const int fd) {struct timespec abstime;auto iter = timeouts.begin();  // 最小超时时间节点if (iter != timeouts.end()) {abstime.tv_sec = iter->expire / 1000;abstime.tv_nsec = (iter->expire % 1000) * 1000000;} else {abstime.tv_sec = 0;abstime.tv_nsec = 0;}struct itimerspec its;its.it_interval = {};its.it_value = abstime;timerfd_settime(fd, TFD_TIMER_ABSTIME, &its, nullptr);}private:static inline uint64_t GenID() { // 生成一个 idreturn gid++;}static uint64_t gid; // 全局 id 变量set<TimerNode, std::less<> > timeouts; // less指定排序方式:从小到大
};

在class timer中实现了

1.创建set容器对象

2.定时任务节点的添加

3.定时任务节点的删除

4.执行已超时任务

四、main函数

int main() {int epfd = epoll_create(1);  // epollint timerfd = timerfd_create(CLOCK_MONOTONIC, 0);struct epoll_event ev = {.events = EPOLLIN | EPOLLET};epoll_ctl(epfd, EPOLL_CTL_ADD, timerfd, &ev);unique_ptr<Timer> timer = make_unique<Timer>();int i = 0;timer->AddTimer(1000, [&](const TimerNode &node) {      //   lamda 表达式cout << Timer::GetTick() << "node id:" << node.id << "revoked times" << ++i << endl;});timer->AddTimer(1000, [&](const TimerNode &node) {cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;});timer->AddTimer(3000, [&](const TimerNode &node) {cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;});auto node = timer->AddTimer(2100, [&](const TimerNode &node) {cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;});timer->DelTimer(node);cout << "now time:" << Timer::GetTick() << endl;struct epoll_event evs[64] = {0};while (true) {timer->UpdateTimerfd(timerfd);    // epoll中timerfd的到期时间int n = epoll_wait(epfd, evs, 64, -1); // 内核检测定时时间timerfdtime_t now = Timer::GetTick();   // 当前系统时间戳for (int i = 0; i < n; i++) {     // for network event handle}timer->HandleTimer(now);         // 处理现在到期的定时任务}epoll_ctl(epfd, EPOLL_CTL_DEL, timerfd, &ev);close(timerfd);close(epfd);return 0;
}

timerfd的使用:

1.将timerfd添加到epoll中

2.使用timerfd_settime()函数更新timerfd的超时时间

3.当超时时间到达时,epoll会通知事件

4.执行完到期的任务后,更新timerfd的超时时间为红黑树中最小节点的超时时间,epoll会再次进行通知

五、完整代码

#include <sys/epoll.h>
#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>#include <functional>
#include <chrono>
#include <set>
#include <memory>
#include <iostream>using namespace std;struct TimerNodeBase { //  定时器节点基类,用于红黑树(set)存储time_t expire;     //  超时时间uint64_t id;       //  唯一 id, 用于解决超时时间相同的节点存储问题
};struct TimerNode : public TimerNodeBase {  // 子类定时器节点, 添加了一个回调函数using Callback = function<void(const TimerNode &node)>;Callback func;TimerNode(int64_t id, time_t expire, Callback func) : func(std::move(func)) { // 使用 move 右值引用,性能高this->expire = expire;this->id = id;}
};bool operator < (const TimerNodeBase &lhd, const TimerNodeBase &rhd) { // 运算符重载,比较两个节点的大小// 先根据超时时间判定大小if (lhd.expire < rhd.expire) {return true;} else if (lhd.expire > rhd.expire) {return false;} // 超时时间相同时,根据 id 判断大小else return lhd.id < rhd.id;
}class Timer {public:static inline time_t GetTick() { // 获取系统当前时间戳return chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now().time_since_epoch()).count();}TimerNodeBase AddTimer(int msec, TimerNode::Callback func) {time_t expire = GetTick() + msec; // msec是相对超时时间,expire是绝对超时时间(时间戳)// 如果待插入节点当前不是红黑树中最大的if (timeouts.empty() || expire <= timeouts.crbegin()->expire) { auto pairs = timeouts.emplace(GenID(), expire, std::move(func)); // emplace是在容器内部生成一个对象并插入到红黑树中,性能优于push的copy操作  2.使用move右值引用,避免copy// 使用static_cast将子类cast成基类return static_cast<TimerNodeBase>(*pairs.first); // emplace的返回值pair包含:1.创建并插入的节点 2.是否成功插入(已存在相同节点则插入失败)}// 如果待插入节点是最大的,直接插入到最右侧,时间复杂度 O(1) ,优化性能auto ele = timeouts.emplace_hint(timeouts.crbegin().base(), GenID(), expire, std::move(func));// 返回基类而不是子类return static_cast<TimerNodeBase>(*ele);}void DelTimer(TimerNodeBase &node) { // 从(set)红黑树中删除一个节点auto iter = timeouts.find(node); // 找到指定节点if (iter != timeouts.end())timeouts.erase(iter);       // 移除}void HandleTimer(time_t now) {     // 执行当前已超时的任务auto iter = timeouts.begin();while (iter != timeouts.end() && iter->expire <= now) {iter->func(*iter);iter = timeouts.erase(iter); // eraser返回下一个节点}}public:// 更新 timerfd 的到期时间为 timeouts 集合中最早到期的定时器时间virtual void UpdateTimerfd(const int fd) {struct timespec abstime;auto iter = timeouts.begin();  // 最小超时时间节点if (iter != timeouts.end()) {abstime.tv_sec = iter->expire / 1000;abstime.tv_nsec = (iter->expire % 1000) * 1000000;} else {abstime.tv_sec = 0;abstime.tv_nsec = 0;}struct itimerspec its;its.it_interval = {};its.it_value = abstime;timerfd_settime(fd, TFD_TIMER_ABSTIME, &its, nullptr);}private:static inline uint64_t GenID() { // 生成一个 idreturn gid++;}static uint64_t gid; // 全局 id 变量set<TimerNode, std::less<> > timeouts; // less指定排序方式:从小到大
};uint64_t Timer::gid = 0;int main() {int epfd = epoll_create(1);  // epollint timerfd = timerfd_create(CLOCK_MONOTONIC, 0);struct epoll_event ev = {.events = EPOLLIN | EPOLLET};epoll_ctl(epfd, EPOLL_CTL_ADD, timerfd, &ev);unique_ptr<Timer> timer = make_unique<Timer>();int i = 0;timer->AddTimer(1000, [&](const TimerNode &node) {      //   lamda 表达式cout << Timer::GetTick() << "node id:" << node.id << "revoked times" << ++i << endl;});timer->AddTimer(1000, [&](const TimerNode &node) {cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;});timer->AddTimer(3000, [&](const TimerNode &node) {cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;});auto node = timer->AddTimer(2100, [&](const TimerNode &node) {cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;});timer->DelTimer(node);cout << "now time:" << Timer::GetTick() << endl;struct epoll_event evs[64] = {0};while (true) {timer->UpdateTimerfd(timerfd);    // epoll中timerfd的到期时间int n = epoll_wait(epfd, evs, 64, -1); // 内核检测定时时间timerfdtime_t now = Timer::GetTick();   // 当前系统时间戳for (int i = 0; i < n; i++) {     // for network event handle}timer->HandleTimer(now);         // 处理现在到期的定时任务}epoll_ctl(epfd, EPOLL_CTL_DEL, timerfd, &ev);close(timerfd);close(epfd);return 0;
}

推荐学习 https://xxetb.xetslk.com/s/p5Ibb

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

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

相关文章

微火问答:全域外卖和本地生活服务是同个项目吗?

当前&#xff0c;本地生活赛道火爆程度不断升级&#xff0c;作为其主要板块之一的团购外卖也持续迸发出新的活力。而全域运营的出现无疑是给团购外卖这把正在熊熊燃烧的烈火&#xff0c;又添了一把新柴&#xff01; 所谓全域运营&#xff0c;简单来说&#xff0c;就是指所有领…

xjar加密springboot的jar包,并编译为执行程序

场景&#xff1a;当前项目需要进行jar包部署在windows环境和linux环境&#xff0c;并要求使用xjar加密。 1. xjar加密 源码程序自行搜索&#xff0c;这里只介绍加密及运行&#xff0c;运行加密程序&#xff0c;指定jar包&#xff0c;输入密码 2. 加密后的目录 3. go程序编译 …

HCIP-Datacom-ARST自选题库__BGP/MPLS IP VPN简答【3道题】

1.在BGP/MPLSIPVPN场景中&#xff0c;如果PE设备收到到达同一目的网络的多条路由时&#xff0c;将按照定的顺序选择最优路由。请将以下内容按照比较顺序进行排序。 2.在如图所示的BGP/MPLSIP VPN网络中&#xff0c;管理员准备通过Hub-Spoke组网实现H站点对VPM流量的集中管控&am…

HNU-人工智能-作业3

人工智能-作业3 计科210X 甘晴void 202108010XXX 1.贝叶斯网络 根据图所给出的贝叶斯网络&#xff0c;其中&#xff1a;P(A)0.5&#xff0c;P(B|A)1&#xff0c; P(B|A)0.5&#xff0c; P(C|A)1&#xff0c; P(C|A)0.5&#xff0c;P(D|BC)1&#xff0c;P(D|B, C)0.5&#xff…

如何将天猫内容保存为PDF格式?详细步骤与实战解析

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言&#xff1a;保存天猫内容的重要性 二、环境准备与工具安装 1. 安装必要的Python包…

宝塔部署Java+Vue前后端分离项目

1. 服务器 服务器选择Linux的CentOS7的版本 2. 宝塔Linux面板 2.1 百度搜索宝塔 2.2 进去之后点击立即免费安装 2.3 选择Linux在线安装&#xff0c;输入服务器信息进行安装(也可以选择其他方式) 安装完成之后会弹一个宝塔的应用面板&#xff0c;并附带有登录名称和密码&…

Linux快速定位日志 排查bug技巧和常用命令

1. 快速根据关键字定位错误信息 grep 在 Linux 系统中&#xff0c;可以使用 grep 命令来查找日志文件中包含特定关键字的行。假设你的日志文件路径为 /var/log/myapp.log&#xff0c;你想要查找包含关键字 "abc" 的日志内容&#xff0c;可以按照以下步骤操作&#…

【机器学习聚类算法实战-5】机器学习聚类算法之DBSCAN聚类、K均值聚类算法、分层聚类和不同度量的聚集聚类实例分析

&#x1f3a9; 欢迎来到技术探索的奇幻世界&#x1f468;‍&#x1f4bb; &#x1f4dc; 个人主页&#xff1a;一伦明悦-CSDN博客 ✍&#x1f3fb; 作者简介&#xff1a; C软件开发、Python机器学习爱好者 &#x1f5e3;️ 互动与支持&#xff1a;&#x1f4ac;评论 &…

【微服务】安装docker以及可视化界面

1.配置yum下载源为aliyun源 yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo2.下载docker不加版本号默认为最新版本 yum install -y docker-ce3.启动以及开机自启 #启动docker命令 systemctl start docker #设置开机自启命令…

软件功能测试的类型和流程分享

在现代社会&#xff0c;软件已经成为人们生活中不可或缺的一部分&#xff0c;而在软件的开发过程中&#xff0c;功能测试是不可或缺的环节。软件功能测试指的是对软件系统的功能进行检查和验证&#xff0c;以确保软件在各种情况下能够正常运行&#xff0c;并且能够按照用户需求…

【CTF Web】CTFShow web5 Writeup(SQL注入+PHP+位运算)

web5 1 阿呆被老板狂骂一通&#xff0c;决定改掉自己大意的毛病&#xff0c;痛下杀手&#xff0c;修补漏洞。 解法 注意到&#xff1a; <!-- flag in id 1000 -->拦截很多种字符&#xff0c;连 select 也不给用了。 if(preg_match("/\|\"|or|\||\-|\\\|\/|\…

【电路笔记】-状态可变滤波器

状态可变滤波器 文章目录 状态可变滤波器1、概述2、**状态可变滤波器电路**3、状态可变滤波器示例4、陷波滤波器设计5、总结状态可变滤波器是一种多反馈滤波器电路,可以从同一单个有源滤波器设计中同时产生所有三种滤波器响应:低通、高通和带通。 1、概述 状态可变滤波器使用…

Vue中使用Vue-scroll做表格使得在x轴滑动

页面效果 首先 npm i vuescroll 在main.js中挂载到全局 页面代码 <template><div class"app-container"><Header :titletitle gobackgoBack><template v-slot:icon><van-icon clickgoHome classicon namewap-home-o /></templat…

HackTheBox-Machines--Beep

Beep测试过程 1 信息收集 nmap端口扫描 gryphonwsdl ~ % nmap -sC -sV 10.129.137.179 Starting Nmap 7.94 ( https://nmap.org ) at 2024-05-28 14:39 CST Nmap scan report for 10.129.229.183 Host is up (0.28s latency). Not shown: 988 closed tcp ports (conn-refused…

DSP6657 GPIO中断学习

1 简介 使用创龙板卡的KEY2按键通过中断的方式控制LED3的亮灭 2 中断学习 在C665x设备上&#xff0c;CPU中断是通过C66x CorePac中断控制器进行配置的。该中断控制器允许最多128个系统事件被编程到任意12个CPU可屏蔽中断输入&#xff08;CPUINT4至CPUINT15&#xff09;、CPU…

如何成为一名合格的JAVA程序员?

如何成为一名称职的Java编程人员&#xff1f;你一定不能错过的两本书。 第一本《Java核心技术速学版&#xff08;第3版&#xff09;》&#xff01; 1.经典Java作品《Java核心技术》的速学版本&#xff0c;降低学习门槛&#xff0c;帮助读者更容易学习Java&#xff0c;更快地把…

HTML+CSS TAB导航栏

效果演示 这段代码实现了一个名为"Tab导航栏"的效果,它是一个基于CSS的导航栏,包含五个选项卡,每个选项卡都有一个带有渐变背景色的滑块,当用户点击选项卡时,滑块会滑动到相应的位置。同时,选中的选项卡会变为白色,未选中的选项卡会变为灰色。 Code <!DOC…

基于FPGA实现LED的闪烁——HLS

基于FPGA实现LED的闪烁——HLS 引言&#xff1a; ​ 随着电子技术的飞速发展&#xff0c;硬件设计和开发的速度与效率成为了衡量一个项目成功与否的关键因素。在传统的硬件开发流程中&#xff0c;工程师通常需要使用VHDL或Verilog等硬件描述语言来编写底层的硬件逻辑&#xff0…

kafka-主题创建(主题操作的命令)

文章目录 1、topic主题操作的命令1.1、创建一个3分区1副本的主题1.1.1、获取 kafka-topics.sh 的帮助信息1.1.2、副本因子设置不能超过集群中broker的数量1.1.3、创建一个3分区1副本的主题1.1.4、查看所有主题1.1.5、查看主题详细描述 1、topic主题操作的命令 kafka发送消息会存…

vue中在mounted使用$refs获取不到DOM元素

vue中在mounted使用$refs获取不到DOM元素 前言解决方案1、通过使用$nextTick来获取2、updated中获取 前言 在使用ref的时候&#xff0c;在mounted中通过$ref获取节点是获取不到报undefined this.$refs.xx 为 undefined 解决方案 在mounted钩子中加载回来的数据不会在这个阶段更…