C++ 定时器

news2024/9/16 9:41:22

    这是第一次独立设计一个模块,从接口定义,模块组合到多线程并发可能遇到的各种问题,虽然定时挺简单的,但是想设计精度高,并且能应对高并发似乎也不是很容易,当然,最后没有测试定时器的代码,不知道能到什么水平,只是想记录整个过程中遇到的问题,和一些思考。

一、模块构成

   其是一开始就想把整个定时器分成三个模块:

  1. ticker:负责每一次滴答的,就是秒针。
  2. taskstruct:用来管理任务,能够插入、删除任务。
  3. wokerpool:同来执行任务的线程(俗称牛马)。
  4. timer:这个定时器模块。

这就是像的最初结构。

1.1 TaskStruct

    taskstruct用来管理各个任务。我第一个想到的就是时间轮算法,timewheel。

大概就是这样子(画得怪丑的.....),插入任务,需要先找到是哪个盘子,然后是哪个slot,插入任务队列。这样面临两个问题

  1. 如何快速的定位到wheel和slot?
  2. 用什么数据管理每个slot的task?

第一个问题:关系到插入的效率。第二个问题:因为当第二个wheel指针移动到下一个slot时,需要将指针指向的slot队列移动到前一个wheel,这个过程如何高效;并且新来的如何如何快速插入到slot中。

对于第一个问题

    采用bitmap的方式快速定位,怎么理解呢?首先linux中时间的精度一般是微妙(当然可以达到纳秒,但是纳秒似乎精度太高了,程序的运行会导致相对误差太大,暂且先用微妙)。通过gettimeofday函数正好获得的也是微妙级。换算过来就是uint64位的数字,表示1900年哒哒哒的东西(具体查资料吧,之前也写过linux的时间库),然后定时时间加入是100微妙,用uint64表示就是

0110 0100

如图中所示,加入每个step是1微妙,每个wheel有8个slot,从0到7;那么第1个wheel可以表示的时间就是

0~7
000 000 000 ~ 000 000 111

这意味着可以用bitmap区索引wheel,和slot,最高位在最后三个bit中,就是在第一个;最高位在中间三个bit中,就是第二个wheel;最高位在前三个bit中,就是在第三个wheel。这个这三个bit的序号就是slot的索引。这样划分bit就能快速定位wheel和slot。

对于第二个问题

用什么样的数据结构slot的数据,最开始我有四个备选项:链表、有序数组、红黑树、跳表。

  1. 链表:可以在O(1)时间插入删除,但是查找时间需要O(n),但是从后一个wheel移动到前一个wheel只要O(1)的时间,只要接在最后就行了。
  2. 有序数组:查找时间是O(log n),但是删除和插入的时间都是O(n),因为要移动数据。从后一个wheel移动到前一个wheel相当于一次查找和插入,因为是有序的,应该是O(n^2)(不知道对不对....)反正总之,有序数组效率很低。
  3. 红黑树:这个数据结构其是不错,但是红黑树的插入删除涉及到树的旋转(虽然不会超过三次,即记得好像是,但是需要从低到高递归变色,插入大概有三种情况,删除分为5种吧,记不清了)关于树的旋转也觉得不是很快。
  4. 跳表:我觉得这个是相对比较均衡的数据结构,有序的链表,插入删除都是logn,合并也是O(n)。并且,想leveldb底层数据不也是跳表吗。

但是,我总觉得,定时器需要考虑大量的删除操作吗?定时任务到期完全可以通过相关状态判断任务是否需要执行,就拿raft算法来说,leader每次都会发送心跳包,而follower收到心跳数据,就会重置计时器,重置计时器要么变为查找,删除,查找,插入操作。其实完全没必要搞这么麻烦,每个follower在变成follower时,记录一个原子变量uint64 i = 0;然后每次添加定时器的时候把i值给定时器,相当于给个票,每次收到心跳,i原子自增,定时器到期后对比拿到的号和i一样吗,一样,表示这段时间没有收到心跳数据,不一样表示收到了。所以,我觉得完全没必要删除定时器,可以根据一个状态位判断任务是否执行。顺便计算一下,一个64位i最大表示是10^19,按心跳包间隔1ns,还需要200年才到期,而这是整个系统完全没有leader和follower变动一直两百年。所以完全不需要担心回到起点的问题。

其次,实际上链表数据结构在slot上是无序的,但是,当一个slot被放到前一个wheel上是,就是一种排序(这大概就是归并排序的思想)随着不断往前,原本的一个slot数据会不断排序,wheel越靠前,slot之间的间隔越小,在第一个wheel上的slot是同一时间的,所以,删除操作还有另一个巧妙的方法,就是在做一定时器,这个定时器只比需要删除的早一点,当他到期时,就执行删除操作,通过时间定位到在第一个wheel的哪个slot,直接删除会快很多,但是这个带来的问题就是第一个wheel上的并发竞争很大,因为第一个wheel是秒针,秒针会很快的转动,要执行任务,还要执行删除,感觉还是不是很合适。

经过上面的分析,决定使用链表,认为不需要删除任务,将任务到期的执行逻辑交给上层应用保证。最终TimerStruct接口如下:

typedef std::function<void()> TimeDelayTask;

struct TimerEntry{
    Timestamp m_oTimeStamp;
    TimeDelayTask m_fTask;
    explicit TimerEntry(Timestamp& timestamp, TimeDelayTask task)
        : m_oTimeStamp(timestamp),
          m_fTask(task){}
    virtual ~TimerEntry() {}
    
    void SetTimestamp(Timestamp timestamp) { m_oTimeStamp = timestamp; }
    void SetTask(TimeDelayTask task) { m_fTask = std::move(task); }
    void SetTask(TimeDelayTask&& task) { m_fTask = std::move(task); }

    Timestamp GetTimestamp() { return m_oTimeStamp; }
    const TimeDelayTask GetTask() { return m_fTask; }
};

class TimerStruct{
public:
    virtual bool DeleteEntry(Timestamp& timestamp) = 0; 
    virtual bool InsertEntry(Timestamp& timestamp, TimeDelayTask& task) = 0;
    virtual bool Start() = 0;
    virtual bool Stop() = 0;
    virtual void Tick() = 0;
    virtual bool Clear() = 0;
    virtual bool Reset(Timestamp& tmstamp) = 0;
    virtual bool IsRunning() = 0;
    
    void RegistToTimer(Timer* tm);

    void UnregistToTimer();

    virtual uint64_t GetTaskNum() = 0;

    virtual ~TimerStruct() {}
protected:
    Timestamp m_uiStartTime;
    Timer* m_opTimer;
    uint64_t m_uiInterval;

};

typedef std::list<TimerEntry*> TimeDelayTaskQueue;

TimerEntry是一个任务,包括一个时间戳和延迟执行的任务。TimerStruct是管理任务的接口类。提供插入、删除、开始、停止、清空、重置、以及挂载到timer上的接口(虽然我认为删除操作是没有必要的,但是,接口还是留着吧,还有这个start和stop和isrunning,实现的时候,我发现好像也不是很需要,计时器的开始与停止完全可以有timer控制,但是,我总觉得,这个东西还是有必要留一下.....)。

1.1.1 timewheel

wheel两两之间通过双向链表连接,没有写一个control去控制所有的wheel,addtask时,先判断是否在第一个wheel上,不在就进入下一个,判断是否早下一个,递归进入。这里其实在实现过程中,我发现这种方式不是很好,应该一个controler用来管理每一个wheel,这样效率会更高点,但是代码没有做(有点懒,后面看机会改),这种分层的wheel提供了一个很好的应对并发的结构,比如秒针只会在第一个wheel上获取锁,而在不同wheel上添加任务的线程可以并发执行,每个wheel的锁独立,这种细粒度的锁就和mysql的锁一样,提供粗粒度到细粒度的锁,实际上,在后面的wheel可以提供更加细粒的的slot锁,但是,这个是需要考虑一个问题的,就是,在iwheel上添加任务是不能让当前的指针移动的,这样对tick来说,一旦发生进位,就要锁住整个wheel,移动指针,移动任务,此时的slot也应该是锁上的,所以,依然会有竞争态,但是相比没有slot锁,竞争会小很多,并发度会高很多(但没有实现,后面有机会再实现吧)。

1.1.2 slot迁移

    当后面的指针移动时,需要将指向的slot的task移动到前一个wheel,这里实际上就是遍历列表插入操作,不过,这个有一个快速通道,就是不需要判断任务是否在前一个wheel上,一定在前一个wheel,所以直接通过与操作获得索引,所以效率比较快。但是需要注意一点是,进位可能是连续的进位,所以在迁移任务的时候必须要从后往前迁移。不过,这里在实现的时候感觉这个timewheel结构有点问题,觉得有一个类似wheel control管理每一个wheel会更高效一点,这里是通过逐层递归的方式。

1.2 ticker

enum TimeUint : uint64_t{
    SECONDS = 1000000,
    MILLISECOND = 1000,
    MICROSECONDS = 1,
};

class Ticker{
public:
    Ticker() : m_uiInterval(100), m_eUint(MICROSECONDS),m_uiMicroInterval(m_uiInterval*m_eUint)  {}
    explicit Ticker(uint64_t interval, TimeUint uint)
        : m_uiInterval(interval), m_eUint(uint),
          m_uiMicroInterval(m_uiInterval*m_eUint)
    {}

    uint64_t GetInterval() const {return m_uiMicroInterval; }
    TimeUint GetUint() const {return m_eUint; }

    virtual void SetInterval(uint64_t interval) {
        m_uiInterval = interval;
        m_uiMicroInterval = m_uiInterval*m_eUint;
    }

    virtual void GetUint(TimeUint uint) {
        m_eUint = uint;
        m_uiMicroInterval = m_uiInterval*m_eUint;
    }

    virtual void tick() = 0;
    virtual void Stop() = 0;

protected:
    uint64_t m_uiInterval;
    TimeUint m_eUint;
    uint64_t m_uiMicroInterval;
};

这个到是没想到一个精准的方案,包括一个间隔和单位。提供tick和stop操作。不过,tick的方式很朴实,要么:

  1. 通过this_thread::sleep_for休眠。
  2. 通过select定时返回,只要select集合里的东西都是NULL就行。
  3. 通过linux的timefd定时,start的时候结束就是UINT64_MAX就行。

查了一些资料,看到的好像是这些方法,似乎没有更加精准的定时方法了。

2.3 workerpool

    用来处理定时任务的线程池。这里,为了减少对锁的竞争,workerpool维护的任务队列是这种形式

 这里是因为,wheel每个slot是指向一个任务队列,当定时任务到时的时候,直接将指向任务队列的指针放到queue末尾。指针的移动能够减小临界区访问长度,这样就不会出现激烈的锁的竞争。而worker取任务的时候,也是直接取走指针。执行完任务,负责delete操作。

2.4 timer

class Timer{
public:
    Timer(uint64_t worknum, Ticker* ticker, TimerStruct* tmstruct);
    ~Timer();
    void RegistTimerStruct(TimerStruct* tmstruct);
    void AddTask(Timestamp& timestamp, TimeDelayTask& task);
    // void AddTask(Timestamp& timestamp, TimeDelayTask&& task);
    void AddTaskToQueue(Timestamp& timestamp, TimeDelayTask& task);
    void AddTaskToQueue(TimeDelayTaskQueue* ptr);
    void DeleteTask(Timestamp& timestamp);

    bool Start();
    bool Stop();
    bool Clear();

    uint64_t GetInterval();
    Timestamp GetTimestamp() { return m_oStartTime; }

 private:
    void Tick();
    inline bool InRunning();

    uint64_t m_iWorkersNum;
    uint64_t m_iInterval;
    std::atomic<uint64_t> m_atoiCount;
    std::atomic<bool> m_bIsRunning;

    Timestamp m_oStartTime;
    Ticker* m_poTicker;
    TimerStruct* m_poTimerStruct;
    WorkerPool* m_poWorkerPool;
//    MutexLock m_oMutex;
};
  • m_iWorkersNum:worker的工作数量。

  • uint64_t m_iInterval:步长,每个tick的间隔。

  • m_atoiCount:每tick一次自增一次,通过这个,可以在这一层对定时任务的时间判断,如果小于定时器时间,可以说定时失败(也可以直接当到队列中,立即执行),等于的任务直接放到任务队列。大于就添加到wheel上。

  • m_bIsRunning:是否在运行中。

  • m_oStartTime:开始时间。

  • m_poTicker:ticker指针。

  • m_poTimerStruct:struct指针。

  • m_poWorkerPool:workerpool指针。

总结

    其实定时器结构到不是很难的结构,主要是想在设计的每一步都考虑如何应对并发的情况,毕竟,即使像产生唯一ID这种如果在并发压力下依然要小心设计。不过,定时器可以为分布式系统服务吗?每台电脑时间不一致,可能还会自动校准时间,不知道怎么应对.......

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

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

相关文章

架构模式:MVC

引言 MVC&#xff0c;即 Model&#xff08;模型&#xff09;-View&#xff08;视图&#xff09;-Controller&#xff08;控制器&#xff09;&#xff0c;是广泛应用于交互式系统中的典型架构模式&#xff0c;尤其在 GUI 和 Web 应用中。 MVC 的概念源自 GOF&#xff08;Gang …

JS解密工具之**如何续期 Charles 的 SSL 证书**

本文由 jsjiami加密/一键JS解密 独家赞助 有问题请私聊加密官方客服 Charles 是一款常用的 HTTP 代理工具&#xff0c;用于调试网络请求。然而&#xff0c;Charles 的 SSL 证书会定期过期&#xff0c;如果 SSL 证书失效&#xff0c;你将无法对 HTTPS 请求进行抓包。本文将详细…

SQL语句中in条件超过1000怎么办?

博客主页: 南来_北往 系列专栏&#xff1a;Spring Boot实战 引言 当遇到SQL语句中IN条件超过1000个的情况时&#xff0c;可以采取以下几种策略来有效处理这一问题&#xff1a; 使用临时表&#xff1a;将IN列表中的值存储在临时表中&#xff0c;并将该临时表与查询表进行J…

【Python 千题 —— 算法篇】寻找最长回文子串

Python 千题持续更新中 …… 脑图地址 &#x1f449;&#xff1a;⭐https://twilight-fanyi.gitee.io/mind-map/Python千题.html⭐ 题目背景 回文串是指一个字符串从左到右和从右到左读都是一样的。寻找一个字符串中的最长回文子串是许多经典算法问题之一&#xff0c;广泛应…

2024年9月最新界面:自己如何在电脑上注册新的Google谷歌账号,图文详解和关键点解析、常见问题

有一些朋友需要通过谷歌账号来工作、学习或娱乐&#xff08;例如很多游戏需要用谷歌账号来注册和使用&#xff09;&#xff0c;但是不知道如何注册谷歌账号&#xff0c;或者知道如何注册&#xff0c;但是对于一些步骤或者注意事项不太熟悉&#xff0c;导致注册不成功&#xff0…

什么是LED智能会议一体机?COB超微小间距LED会议一体机大势所趋

LED智能会议一体机&#xff0c;作为现代会议室革新的核心装备&#xff0c;正逐步颠覆传统会议模式的界限。它不仅仅是一台集成了高清显示、触控互动、音视频处理及远程协作等功能于一体的智能设备&#xff0c;更是推动会议效率与体验双重飞跃的关键力量。随着技术的不断进步&am…

【重学 MySQL】十八、逻辑运算符的使用

【重学 MySQL】十八、逻辑运算符的使用 AND运算符OR运算符NOT运算符异或运算符使用 XOR 关键字使用 BIT_XOR() 函数注意事项 注意事项 在MySQL中&#xff0c;逻辑运算符是构建复杂查询语句的重要工具&#xff0c;它们用于处理布尔类型的数据&#xff0c;进行逻辑判断和组合条件…

【Protobuf】初识protobuf以及详细安装教程

W...Y的主页 &#x1f60a; 代码仓库分享 &#x1f495; 目录 序列化概念 ProtoBuf是什么 ProtoBuf在window下的安装 下载ProtoBuf编译器 配置环境变量 ​编辑 检查是否配置成功 ​编辑 ProtoBuf在Linux下的安装 下载ProtoBuf 安装ProtoBuf 序列化概念 首先我们…

小白开发中遇到的问题和解决方案

小白开发中遇到的问题和解决方案 文章目录 小白开发中遇到的问题和解决方案问题一 问题一 问题&#xff1a;端口别占用可能开开启多个应用 解决方法–在cmd执行下方红框中的命令关闭所有应用

MyBatis-MappedStatement什么时候生成?QueryWrapper如何做到动态生成了SQL?

通过XML配置的MappedStatement 这部分MappedStatement主要是由MybatisXMLMapperBuilder进行解析&#xff0c;核心逻辑如下&#xff1a; 通过注解配置的MappedStatement 核心逻辑就在这个里面了&#xff1a; 继承BaseMapper的MappedStatement 我们看看这个类&#xff0c;里…

idea如何配置模板

配置生成代码指令模板 注&#xff1a;我们常用的有sout,main等指令 第一步打开设置面板 1)按如下操作 2&#xff09;或者CtrlAltS快捷键直接弹出 第二步找 Editor>LiveTemplates 如下图 第三步创建模板 步骤如下 1&#xff09;创建分组名字 2)分组名字 3&#xff09;创…

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p

pycharm如何安装selenium

在pycharm中打开一个项目后,点击Setting(ALTCtrlS快捷键) 然后点击install package完成后点击关闭这个窗口,就可以在代码中使用selenium了 成功后出现如下界面 编写一段正常可以运行操作chorme浏览器的 from selenium import webdriver # 指定ChromeDriver的路径driver we…

关于 PC打开“我的电脑”后有一些快捷如腾讯视频、百度网盘、夸克网盘、迅雷等各种捷方式在磁盘驱动器上面统一删除 的解决方法

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/142029325 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…

淘宝开放平台交易类API解析以及如何测试?

调用淘宝开放平台的订单接口&#xff0c;主要可以通过以下几种途径进行&#xff1a; 1. 直接使用淘宝开放平台提供的API接口 步骤概述&#xff1a; 注册淘宝开放平台账号&#xff1a;首先&#xff0c;你需要在淘宝开放平台注册一个开发者账号。创建应用&#xff1a;在注册并…

Unity3D 小案例 像素贪吃蛇 01 蛇的移动

Unity3D 小案例 像素贪吃蛇 第一期 蛇的移动 像素贪吃蛇 今天来简单制作一个小案例&#xff0c;经典的像素贪吃蛇。 准备 首先调整一下相机的设置&#xff0c;这里使用灰色的纯色背景&#xff0c;正交视图。 接着&#xff0c;创建一个正方形&#xff0c;保存为预制体&#…

位运算技巧总结

一、常见位运算操作 1、基础位运算 & 按位与 有0则0 | 按位或 有1则1 ^ 按位异或 相同为0 不同为1 2、确定数n的二进制位中第x位是0还是1 目的&#xff1a;是0返回0&#xff0c;是1返回1 (n >> x) & 1 思路&#xff1a;1除了第一位其他位都是0&a…

01初识FreeRTOS【前情回顾篇】

为什么要使用FreeRTOS&#xff1f; 裸机轮询无法避免两个函数相互影响的问题&#xff0c;例如我们使用单片机在进行裸机开发时&#xff0c;我们使用了Delay延时函数&#xff0c;这时我们无法再执行其他的功能代码&#xff0c;需要等延时时间结束再执行其他代码&#xff0c;而使…

通过域名无法访问不到网站,IP可正常访问(DNS污染)

一 DNS被污染 就在刚刚突然访问不到csdn&#xff0c;域名无法访问如下图&#xff1a; 确认DNS是否解析有问题 1 ping 域名 先ping一下域名&#xff0c;ping 域名后得到ip, ping通了如下图&#xff1a; 2 使用IP访问测试 通过ip再访问网站&#xff0c;ip可以正常访问如下图&…

nginx搭配gateway的集群配置

一、nginx在http里配置如下信息 upstream gateway-cluster {server 127.0.0.1:10001;server 127.0.0.1:10002;}server {listen 1000;server_name localhost;location ~/zzw_project/(.*) {proxy_pass http://gateway-cluster/$1;proxy_set_header Host $host; # 代理设…