当前位置: 首页 > news >正文

定时器的实现方案:红黑树、最小堆与时间轮

引言

在计算机编程领域,定时器是一个非常重要的组件,它可以在指定的时间点触发特定的任务。常见的定时器实现方案有红黑树、最小堆和时间轮,每种方案都有其独特的优缺点和适用场景。本文将深入探讨这三种定时器实现方案,分析它们的原理、性能和适用场景。

一.定时器的方方面面(定义)

1.定时器的定义和应用

①.定时器定义

组织大量定时任务模块,项目底层基础模块。

②.定时器应用

解决延时处理任务问题,如客户端与服务端连接检测。

③.心跳检测:

包括keep alive和应用层发送心跳包,区别在于检测范围和网络通畅性。

④.游戏开发中的定时器:

技能冷却和倒计时,需要高性能定时器。

2.定时器实现

①.定时器组成:

数据结构和驱动方式。

②.数据结构:

按触发时间排序或按执行序组织。

③.驱动方式:

包括reactor网络模型、time fd和帧更新。

3.选择不同的数据结构(红黑树/最小堆/时间轮)

红黑树

①.红黑树定义:

平衡二叉搜索树,有序结构。

②.红黑树特征:

有序、平衡、黑节点高度一致。

③.红黑树操作:

增删改查,左旋转右旋转,重新着色。

④.红黑树在定时器中的应用:

组织定时任务,保持平衡。

最小堆

①.最小堆定义:

完全二叉树,满足父子关系。

②.最小堆特征:

最小值为根节点,父子节点大小关系约束。

③.最小堆操作:

添加节点和删除节点,时间复杂度为O(log n)。

④.最小堆在定时器中的应用:

快速找到最小值,驱动定时器运行。

时间轮

①.时间轮定义:

按时间指针移动触发任务的机制。

②.时间轮特征:

适用于多线程环境,优势明显。

③.时间轮操作:

添加任务、删除任务、移动时间指针。

④.时间轮在定时器中的应用:

处理海量定时任务。

时间轮本质上是一个环形的数组,每个数组元素代表一个时间间隔,例如1秒、1分、1时等。时间轮通过指针的旋转来管理和执行定时任务,每个槽通常使用双向链表来存储该时间点需要执行的任务,以便高效地添加、删除和遍历任务。

二.基于红黑树容器set实现一个定时器集合(代码)

#include <sys/epoll.h>
#include <sys/timerfd.h>
#include <time.h> // for timespec itimerspec
#include <unistd.h> // for close#include <functional>
#include <chrono>
#include <set>
#include <memory>
#include <iostream>using namespace std;//(基类)父类
struct TimerNodeBase {time_t expire;uint64_t id; 
};//(派生类)子类
struct TimerNode : public TimerNodeBase {using Callback = std::function<void(const TimerNode &node)>;// 定义一个类型别名 Callback,表示一个接收 const TimerNode& 参数并返回 void 的函数类型Callback func;// 成员变量:一个 Callback 类型的函数对象// 构造函数:接收三个参数,并初始化 TimerNode 的成员TimerNode(int64_t id, time_t expire, Callback func) : func(func) {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;} 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;//它首先计算定时器的过期时间 expire,即当前时间加上 msec。if (timeouts.empty() || expire <= timeouts.crbegin()->expire) {//如果 timeouts 集合为空,或者新定时器的过期时间早于或等于当前最迟的定时器,它就将定时器添加到集合中。auto pairs = timeouts.emplace(GenID(), expire, std::move(func));return static_cast<TimerNodeBase>(*pairs.first);}auto ele = timeouts.emplace_hint(timeouts.crbegin().base(), GenID(), expire, std::move(func));return static_cast<TimerNodeBase>(*ele);}void DelTimer(TimerNodeBase &node) {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);}}public:virtual void UpdateTimerfd(const int fd) {struct timespec abstime;// 定义一个 timespec 结构体,用来保存定时器的绝对时间auto iter = timeouts.begin(); // 获取当前最早的定时器(即过期时间最小的定时器)if (iter != timeouts.end()) {    // 如果 timeouts 集合中有定时器abstime.tv_sec = iter->expire / 1000;// 过期时间的秒部分abstime.tv_nsec = (iter->expire % 1000) * 1000000;// 过期时间的毫秒部分转为纳秒} else {// 如果没有定时器,设置 abstime 为 0,表示没有定时器触发abstime.tv_sec = 0;abstime.tv_nsec = 0;}// 设置 itimerspec 结构体,指定定时器的触发时间struct itimerspec its = {.it_interval = {},// 设定定时器不重复触发,因此它的间隔时间设为 0.it_value = abstime // 设置定时器触发的绝对时间};timerfd_settime(fd, TFD_TIMER_ABSTIME, &its, nullptr);// 使用 timerfd_settime 来设置定时器文件描述符的时间// fd: 定时器文件描述符// TFD_TIMER_ABSTIME: 使用绝对时间而不是相对时间// &its: 指向 itimerspec 结构体的指针,定义定时器的触发时间和间隔时间}private:static inline uint64_t GenID() {return gid++;}static uint64_t gid; set<TimerNode, std::less<>> timeouts;
};
uint64_t Timer::gid = 0;int main() {int epfd = epoll_create(1);int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);//timerfd 是 Linux 下的一种定时器机制,用于定时事件的通知。它通过文件描述符提供与定时器相关的操作。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) {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);int n = epoll_wait(epfd, evs, 64, -1);time_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;
}

①基类 TimerNodeBase

time_t expire :定时器的过期时间 = 当前时间+设定时间

id:定时器模块的id

②子类 TimerNode 以public的方式继承基类

在原本基类里面增加唠嗑一个回调函数Callbaakc func

③小于号运算符重载(应用于红黑树实现从小到大排序)

④定义一个Timer类管理定时器的接口

GenID:获取id编号的私有接口

set<TimerNode,std::less<>>timeouts:节点类型为TimerNode 中序遍历按从小到大排序的一颗红黑树

⑤获取当前(毫秒级)时间的成员函数

⑥增加定时器模块的成员函数

如果 timeouts 集合为空,或者新定时器的过期时间早于或等于当前最迟的定时器,它就将定时器添加到集合中。

如果新定时器的过期时间一直大于当前最迟的定时器 则一直把它添加在红黑树的最右边

⑦删除指定的定时器模块

⑧处理过期的定时器模块 

func 回调函数的作用就是让定时器到期时,能够自定义处理逻辑(如打印日志、执行其他操作等

//传入时间之后,进行判断,小于这个时间的全部进行执行

//将这个删除之后会赋值一下下一个迭代

⑨更新timefd文件描述符

⑩把timerfd加入到epoll的监听集合中

⑩①调用AddTimer函数加入几个定时器模块

⑩②时间到了处理事件重新设定超时时间

⑩③清理工作


http://www.mrgr.cn/news/97310.html

相关文章:

  • 自动化备份全网服务器数据平台
  • go简化版面试题
  • 蓝桥杯高频考点——经典01背包问题详解(附例题)
  • Java 常用数据结构详解
  • Business English Certificates (BEC) 高频词汇背诵
  • 【NLP 54、大模型训练相关知识】
  • Android学习总结之handler源码级
  • Android学习总结之Kotlin 协程
  • LeetCode算法题(Go语言实现)_31
  • C++自学笔记---数组和指针的异同点
  • 【计算机网络】Linux配置SNAT/DNAT策略
  • Android学习总结之算法篇五(字符串)
  • Java基础-设计模式详解
  • [项目总结] 在线OJ刷题系统项目技术应用(上)
  • 燕山大学计算机网络实验(包括实验指导书)
  • 7B斗671B:扩散模型能否颠覆自回归霸权?
  • 网络编程—TCP/IP模型(TCP协议)
  • 通过Postman和OAuth 2.0连接Dynamics 365 Online的详细步骤
  • Java 进化之路:从 Java 8 到 Java 21 的重要新特性
  • 数据库原理