前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >快速上手:用最小堆实现高效通用的定时器组件

快速上手:用最小堆实现高效通用的定时器组件

作者头像
开源519
发布于 2025-02-27 02:07:07
发布于 2025-02-27 02:07:07
10700
代码可运行
举报
文章被收录于专栏:开源519开源519
运行总次数:0
代码可运行

用最小堆实现高效通用的定时器组件

开篇

  在程序开发过程中,定时器会经常被使用到。而在Linux应用开发中,系统定时器资源有限,进程可创建的定时器数量会受到系统限制。假如随便滥用定时器,会导致定时器资源不足,其他模块便无法申请到定时器资源。   如上,假如同一进程中多个模块,需要同时申请不同周期定时器,就会导致模块创建定时器失败。

解决方案

  为解决定时器资源紧缺的问题,通常有以下几种方案:

  • 最小堆方式 ① 首先创建一个系统定时器,设置为一次性触发。 ② 其次基于二叉堆数据结构,将每个定时任务按照时触发时间戳先后顺序依次排列。 ③ 每次取堆顶定时器任务时间戳,计算出触发时间,启动并更新系统定时器触发时间。 ④ 定时器触发后,检查堆顶部的定时任务是否超时,超时触发对应事件,将定时器任务移除堆顶,重复③。(若定时任务为周期任务,则将其按照下次触发时间戳插入至二叉堆)
  • 时间轮方式 ① 首先创建一个系统定时器,设置为周期性触发,周期为多个定时任务可共用的最小颗粒度。 ② 定义环形数组,将时间划分为多个槽,每个槽放多个定时任务。 ③ 定时器按照周期触发,触发后遍历每个槽的定时任务,并触发对应事件。

两者相比,各有优劣。最小堆方式精度更高,时间轮方式则胜在效率。在定时任务数量不庞大的情况下,最小堆方式更合适。本篇主要介绍最小堆的实现。

类图

  通过对定时器功能的理解,可以将其抽象为三个类:系统定时器,定时器任务,定时器任务管理。其类图如下:

定时器管理组件

  • 系统定时器(SystemTimer) 负责封装Linux 定时器接口,向外提供系统定时器的使用接口。主要包含如下功能: ① 创建定时器 ② 启动定时器 ③ 停止定时器 ④ 销毁定时器资源
  • 定时器任务(Timer) 负责缓存定时任务属性的数据结构。主要包含如下数据: ① 触发时间间隔 ② 下次触发时间戳 ② 触发次数 ③ 已触发次数计数 ④ 定时器触发响应事件 ⑤ 预定定时器的模块ID
  • 定时器任务管理(TimerManager) 负责持有系统定时器和定时任务的管理。主要包含如下功能: ① 初始化、启动、结束、销毁系统定时器 ② 接收和缓存定时任务预约事件 ③ 维护定时任务容器,按照定时任务容器时间序更新系统定时器触发时间

源码实现

编程环境

  1. 编译环境: Linux环境
  2. 语言: C++语言

接口定义

  • 系统定时器(SystemTimer)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class SprSystemTimer : public SprObserver
{
public:
    SprSystemTimer(ModuleIDType id, const std::string& name, std::shared_ptr<SprMediatorProxy> mediatorPtr);
    ~SprSystemTimer();
    SprSystemTimer(const SprSystemTimer&) = delete;
    SprSystemTimer& operator=(const SprSystemTimer&) = delete;
    SprSystemTimer(SprSystemTimer&&) = delete;
    SprSystemTimer& operator=(SprSystemTimer&&) = delete;

    int ProcessMsg(const SprMsg& msg);

    int Init();
    int InitTimer();
    int StartTimer(uint32_t intervalInMilliSec);
    int StopTimer();
    int DestoryTimer();

private:
    bool mTimerRunning;
    int  mTimerFd;
};
  • 定时器任务(Timer)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class SprTimer
{
public:
    SprTimer(uint32_t moduleId, uint32_t msgId, uint32_t repeatTimes, uint32_t delayInMilliSec, uint32_t intervalInMilliSec);
    SprTimer(const SprTimer& timer);
    ~SprTimer();

    bool operator < (const SprTimer& t) const;
    bool IsExpired() const;
    uint32_t GetTick() const;
    uint32_t GetModuleId() const { return mModuleId; }
    uint32_t GetMsgId() const { return mMsgId; }
    uint32_t GetIntervalInMilliSec() const { return mIntervalInMilliSec; }
    uint32_t GetExpired() const { return mExpired; }
    uint32_t GetRepeatTimes() const { return mRepeatTimes; }
    uint32_t GetRepeatCount() const { return mRepeatCount; }

    void SetExpired(uint32_t expired) { mExpired = expired; }
    void RepeatCount() const { mRepeatCount++; }

private:
    uint32_t mModuleId;
    uint32_t mMsgId;
    uint32_t mIntervalInMilliSec;
    uint32_t mExpired;
    uint32_t mRepeatTimes;
    mutable uint32_t mRepeatCount;
};
  • 定时器任务管理(TimerManager)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class SprTimerManager : public SprObserver
{
public:
    virtual ~SprTimerManager();
    int Init();
    static SprTimerManager* GetInstance(ModuleIDType id, const std::string& name, std::shared_ptr<SprMediatorProxy> mediatorPtr, std::shared_ptr<SprSystemTimer> systemTimerPtr);
private:
    SprTimerManager(ModuleIDType id, const std::string& name, std::shared_ptr<SprMediatorProxy> mediatorPtr, std::shared_ptr<SprSystemTimer> systemTimerPtr);

    int DeInit();
    int InitSystemTimer();
    int ProcessMsg(const SprMsg& msg) override;
    int PrintRealTime();

    // --------------------------------------------------------------------------------------------
    // - Module's timer book manager functions
    // --------------------------------------------------------------------------------------------
    int AddTimer(uint32_t moduleId, uint32_t msgId, uint32_t repeatTimes, int32_t delayInMilliSec, int32_t intervalInMilliSec);
    int AddTimer(const SprTimer& timer);
    int DelTimer(const SprTimer& timer);
    int UpdateTimer();
    int CheckTimer();
    uint32_t NextExpireTimes();

    // --------------------------------------------------------------------------------------------
    // - Message handle functions
    // --------------------------------------------------------------------------------------------
    void MsgRespondStartSystemTimer(const SprMsg &msg);
    void MsgRespondStopSystemTimer(const SprMsg &msg);
    void MsgRespondAddTimer(const SprMsg &msg);
    void MsgRespondDelTimer(const SprMsg &msg);
    void MsgRespondSystemTimerNotify(const SprMsg &msg);
    void MsgRespondClearTimersForExitComponent(const SprMsg &msg);

private:
    bool mEnable;                                       // Component init status
    std::set<SprTimer> mTimers;                         // sort by SprTimer.mExpired from smallest to largest
    std::shared_ptr<SprSystemTimer> mSystemTimerPtr;    // SysTimer object
};

TimerManager 中存储定时任务的容器用的std::set<Timer>,可以自定义按照时间戳从小到大排序,就不用自己实现二叉堆结构了。

如下是TimerManager中定时器触发的业务逻辑代码: ① 定时器触发后,从头遍历任务容器。 ② 若当前任务已超时且任务未失效,通知定时器触发事件。将当前任务缓存至失效容器,若为重复定时器,更新时间戳,再次插入任务容器。 ③ 若当前任务未到期(说明后续任务都未到期),退出容器遍历。与②互斥。 ④ 从任务容器中,删除②中缓存的失效容器。 ⑤ 当前任务容器若为空,停止系统定时器。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void SprTimerManager::MsgRespondSystemTimerNotify(const SprMsg &msg)
{
    set<SprTimer> deleteTimers;

    // loop: Execute the triggered timers, timers are sorted by Expired value from smallest to largest
    for (auto it = mTimers.begin(); it != mTimers.end(); ++it) {
        if (it->IsExpired()) {
            if (it->GetRepeatTimes() == 0 || (it->GetRepeatCount() + 1) < it->GetRepeatTimes()) {
                SprTimer t(*it);

                // loop: update timer valid expired time
                uint32_t tmpExpired = t.GetExpired();
                do {
                    tmpExpired += t.GetIntervalInMilliSec();
                    t.RepeatCount();
                } while (tmpExpired < it->GetTick());

                if (it->GetRepeatTimes() == 0 || (it->GetRepeatCount() + 1) < it->GetRepeatTimes()) {
                    t.SetExpired(tmpExpired);
                    AddTimer(t);
                }
            }

            // Notify expired timer event to the book component
            SprMsg msg(it->GetModuleId(), it->GetMsgId());
            NotifyObserver(msg);
            it->RepeatCount();

            deleteTimers.insert(*it);
        } else {
            break;
        }
    }

    // Delete expired timers
    for (const auto& timer : deleteTimers) {
        DelTimer(timer);
    }

    // Set next system timer
    uint32_t msgId = mTimers.empty() ? SIG_ID_TIMER_STOP_SYSTEM_TIMER : SIG_ID_TIMER_START_SYSTEM_TIMER;
    SprMsg sysMsg(msgId);
    SendMsg(sysMsg);
    // SPR_LOGD("Current total timers size = %d\n", (int)mTimers.size());
}

测试

测试一个2s的定时器:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
56 DebugCore D: msg id: SIG_ID_DEBUG_TIMER_TEST_2S 2024-03-03 19:26:16.586
56 DebugCore D: msg id: SIG_ID_DEBUG_TIMER_TEST_2S 2024-03-03 19:26:18.586
56 DebugCore D: msg id: SIG_ID_DEBUG_TIMER_TEST_2S 2024-03-03 19:26:20.586
56 DebugCore D: msg id: SIG_ID_DEBUG_TIMER_TEST_2S 2024-03-03 19:26:22.585

总结

  • 对于定时器容器,本篇用到了STL接口的std::set<Timer>容器,通过重载Timer运算符<,实现按照时间戳(mExpired)从小到大排序。
  • 将定时器任务抽象处三个类,各自负责自己的业务,逻辑上更加清晰明了。
  • 使用一个系统定时器资源,完成所有定时任务的响应。实现基础功能的同时,降低对系统定时资源的消耗。

最后

用心感悟,认真记录,写好每一篇文章,分享每一框干货。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-03-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开源519 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
如何实现一个定时器?
定时器在各种场景都需要用到,比如游戏的Buff实现,Redis中的过期任务,Linux中的定时任务等等。顾名思义,定时器的主要用途是执行定时任务。
范蠡
2021/04/08
1.6K0
如何实现一个定时器?
深入Linux C/C++ Timer定时器的实现核心原理
我曾以为像定时器这样基础的功能,操作系统会有一个完备的实现。当需要开启一个定时任务的时候,会有一个优雅的、如下形式的接口:
sunsky
2020/12/21
11.5K1
深入Linux C/C++ Timer定时器的实现核心原理
【项目日记】仿mudou的高并发服务器 --- 整体框架搭建 ,实现时间轮模块
项目地址在这里: https://gitee.com/penggli_2_0/TcpServer
叫我龙翔
2024/11/15
1600
【项目日记】仿mudou的高并发服务器 --- 整体框架搭建 ,实现时间轮模块
时间驱动:探索计时器方案和革命性的时间轮技术
(1)心跳检测 (2)游戏中的技能冷却 (3)倒计时 (4)其他需要延迟处理的功能 定时器,就是指定某个时间处理某个事情。
Lion 莱恩呀
2024/09/21
1670
时间驱动:探索计时器方案和革命性的时间轮技术
掌握C++定时器:构建自己的定时器的分步指南
在c++中,set、map、multiset、multimap使用的是红黑树管理数据。可以利用这几个类实现定时器方案,以set为例,使用C++ 14特性。
Lion 莱恩呀
2024/09/22
3280
掌握C++定时器:构建自己的定时器的分步指南
基于Asio库的定时器,封装实现好用的定时任务
a cross-platform C++ library for network。
杨永贞
2022/04/13
2.3K0
基于Asio库的定时器,封装实现好用的定时任务
Swoole 源码分析之 Timer 定时器模块
Swoole 中的毫秒精度的定时器。底层基于 epoll_wait 和 setitimer 实现,数据结构使用最小堆,可支持添加大量定时器。
码农先森
2024/07/01
820
Go中定时器实现原理及源码解析
我在春节期间写了一篇文章有关时间轮的:https://www.luozhiyun.com/archives/444。后来有同学建议我去看看 1.14版本之后的 timer 优化。然后我就自己就时间轮和 timer 也做了个 benchmark:
luozhiyun
2021/03/07
1.4K0
C++项目:在线五子棋对战网页版--session管理模块开发
在WEB开发中,HTTP协议是⼀种⽆状态短链接的协议,这就导致⼀个客⼾端连接到服务器上之后,服务器不知道当前的连接对应的是哪个用户,也不知道客⼾端是否登录成功,这时候为客⼾端提所有服务是不合理的。因此,服务器为每个用户浏览器创建⼀个会话对象(session对象),注意:⼀个浏览器独占⼀个session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使⽤浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,识别该连接对应的用户,并为用户提供服务。
二肥是只大懒蓝猫
2023/10/13
3130
C++项目:在线五子棋对战网页版--session管理模块开发
Go 并发编程与定时器
在最近的日常后台开发中经常遇到定时任务的需求,如定时通知、定时检查等重要的需求,绝对时间一定不会是完全准确的,它对于一个运行中的分布式系统其实没有太多指导意义,但是由于相对时间的计算不依赖于外部的系统,所以它的计算可以做的比较准确,这里简单总结一下定时任务在Go中的实现
Kevinello
2022/08/19
6660
从零手写操作系统之RVOS软件定时器实现-08
本系列参考: 学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春 整理而来,主要作为xv6操作系统学习的一个前置基础。
大忽悠爱学习
2023/10/11
2620
从零手写操作系统之RVOS软件定时器实现-08
libuv的定时器原理源码解析
执行定时器的时候首先会先移除该定时器,然后如果设置了repeat的话,再次加入到最小堆里,最后执行超时回调。这里有个需要注意的是设置了repeat的定时器,意思是timeout时间后触发第一次超时,后面每隔repeat的时间,触发一次超时。
theanarkh
2019/03/15
1.5K0
libuv的定时器原理源码解析
GO的定时器Timer 和定时任务cron
要是对GO 中 swaggo 的应用还有点兴趣的话,可以查看文章 工作中后端是如何将API提供出去的?swaggo很不错
阿兵云原生
2023/02/16
1.2K0
基于腾讯云AI代码助手辅助实现一个C++定时器类的功能实现
最近注意到了腾讯云AI代码助手这款辅助编码工具,正好自己又有一个项目上的小需求,故决定将其求助于AI实现。
晨星成焰
2024/08/09
1560
基于腾讯云AI代码助手辅助实现一个C++定时器类的功能实现
一种定时器的实现
注册一个时间间隔为 Interval 后执行 ExpiryAction 的定时器实例,其中,返回 TimerId 以区分在定时器系统中的其他定时器实例。
changan
2020/11/04
5660
一种定时器的实现
【项目日记】仿mudou的高并发服务器 --- 实现基础高并发服务器基础模块
实现高并发服务器的基础是实现基于事件触发的Reactor模型,通过Reactor模型对事件进行统一管理。对此我们需要设计:
叫我龙翔
2024/11/24
1530
【项目日记】仿mudou的高并发服务器 --- 实现基础高并发服务器基础模块
关于muduo网络库的注解
http://blog.csdn.net/liuxuejiang158blog/article/details/17056537#comments
bear_fish
2018/09/19
7920
4步实现MQTT客户端与OneNet高效连接
  开源项目Sparrow 的基础框架搭建已接近完成,中间件的基础功能大多已经具备。为了验证该框架的实用性,在工程中引入了业务模块OneNetMqtt。从模块命名可以推断其主要功能是通过MQTT 协议连接OneNet 平台。   最初接触OneNet 还是在大学期间,当时的毕业设计基于OneNet 实现了环境数据采集系统。由于当时的个人水平限制,并未采用MQTT协议实现,功能上体现的效果也不尽预期。现在重新构建此功能,弥补了旧时自身能力的不足,新的实现过程更为高效,连接和数据传输都相当稳定。本篇大致介绍一下功能和主要模块,后续根据需要补充。
开源519
2025/02/27
3380
4步实现MQTT客户端与OneNet高效连接
OpenServer是一款超轻量、超迷你、Actor模式、组件设计的高性能、高并发的跨全平台服务器框架
OpenServer是一款超轻量、超迷你、Actor模式、组件设计的高性能、高并发的跨全平台服务器框架。
linyouhappy
2023/04/05
1.6K0
OpenServer是一款超轻量、超迷你、Actor模式、组件设计的高性能、高并发的跨全平台服务器框架
nodejs事件循环阶段之定时器
上一篇分析了prepare阶段,check和idle阶段是一样的,所以就不分析了。今天分析定时器阶段。nodejs中setTimeout和setInterval就是使用libuv的定时器阶段实现的。libuv中,定时器是以最小堆实现的。即最快过期的节点是根节点。我看看定时器的数据结构。
theanarkh
2020/03/12
1.2K0
推荐阅读
相关推荐
如何实现一个定时器?
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验