• Timer,时间堆


    前言

    博主这一段时间都没有更新博客,因为去 写了几个小的项目。接下来的几篇博客就当作是项目的总结吧。其中一个项目就是来自github的C++11版本WebServer。我实现的则放到了gitee上my_webserver

    该项目应该也是吸取了最经典的TinyWebServer,主要由以下几个模块组成:

    • 配置模块,一些基础的个性化配置。
    • 日志模块,记录服务器信息,便于查看和修bug。
    • 连接池,项目使用mysql数据库,使用连接池管理mysql连接。
    • 线程池,执行任务。
    • 时间堆,用于管理定时任务。
    • epoller,linux下实现高并发的关键。
    • 缓冲区,用于存储http请求和响应的数据。
    • http模块,主要包括http请求,http响应,和管理http连接。
    • webserver,服务器的主要逻辑。

    接下来我会逐一分析markparticle的C++11版本的这些模块。在开始此系列博客之前,强烈推荐大家阅读游双大佬的《Linux高性能服务器编程》,关于这些模块的条条杠杠,不敢说100%,一大半的基础知识都来自这本书。

    什么是定时器

    服务器中的定时器和我们日常生活中的定时器概念一样,**即将一个事件与一个时间点绑定,时间点一到,就执行该事件。**比如,我想明天8点起床,就定了一个8点的定时器。当闹钟一响,我就会执行起床这个事件。那么,在编程中,时间点很容易表示,如何刻画一个事件呢?没错,就是函数。

    void get_up(){}
    
    • 1

    get_up
    这样就能在特定的时间点,执行特定的事件。

    当然了,基于不同的任务,我们的时间点设置也会不同,也可能会有半小时以后,2天以后这样的相对时间点,也可能会有绝对时间点。

    定时器的实现

    服务器往往需要很多个定时任务,这时候就需要一种数据结构管理它们,这就是服务器需要时间管理器的原因。

    如果有很多个定时任务,应该怎样管理它们呢?

    显然,我们需要基于它们触发的时间先后进行排序,排在前面的事件先触发。而触发的规则又有不同。比如,可以让时间管理器按照一定的周期进行触发,每隔5s触发一次之类的。但是,这样的坏处就是,可能每次触发不一定有事件就绪,白白触发。基于这样一种考虑,我们设法获得下一次触发任务到现在的时间t,然后让时间管理器经过t之后去触发任务,此时至少有一个任务会被触发。

    时间堆

    作者实现的时间管理器使用的是堆。利用堆的性质,每次可以选取出最值的特点,每次选择时间节点最小的定时器出来执行,以此往复。
    而关于时间方面,使用的是C++11引入的chrono库。

    typedef std::function<void()> TimeoutCallBack;   //回调函数
    typedef std::chrono::high_resolution_clock Clock; //时钟
    typedef std::chrono::milliseconds MS;    //毫秒
    typedef Clock::time_point TimeStamp;     //时间戳
    
    struct TimerNode{  //时间节点
      int id;
      TimeStamp expires;    //绝对时间
      TimeoutCallBack cb;
      
      bool operator<(const TimerNode& rhs){   //用于比较
        return expires < rhs.expires;
      }
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这里,先typedef了一些类型用于后续方便使用。也定义了时间结点。你可能会注意到,回调函数的类型是function,一个没有参数且没有返回值的函数。那么,如果你的定时任务需要参数怎么办?

    我们默认回调函数没有返回值。因为回调函数可能带有任意类型的参数,所以干脆将其变成没有参数的,如果你的回调函数带有参数,你需要自己使用bind或者lambda进行封装。这个手法经常使用。

    • id,用于标识一个时间结点,用来调整或者删除。
    • expires,就是定时器触发的时间,这里使用的是时间戳。
    • cb,即时间到之后要执行的任务。

    对于时间堆,我们采用数组形式。在插入,调整,删除时间堆时,往往需要获得它们的下标,所以使用哈希存储时间节点id到数组下标的映射关系。

    class Timer{
    public:
    //...
    private:
    std::vector<TimerNode> heap_;
    std::unordered_map<int, int> ref_; //id -> index
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    而对于堆,自然要提供向下调整和向上调整的函数。而为了更好的调整ref_,手写了一个swap函数。

    //向下调整法,在[index, n)中调整。不包括n!!!
    bool Timer::SiftDown_(int index, size_t n);
    
    //向上调整,原作者的size_t类型的index,可能在这里会有一个bug。
    void Timer::SiftUp_(int index);
    
    //交换下标i,j处的值,并调整ref_的映射。
    void Timer::swap(int i, int j);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 时间管理器肯定要提供一个增加定时器的函数add。该函数会向堆中增加一个定时器,如果该定时器已经存在,那么就更新该定时器。
    • 也提供了调整定时器时间的函数,用新的时间刷新指定定时器。
    • 删除定时器,暴露给外部的接口只会删除堆顶节点。该函数回去调用del_。
    //增加定时器,如果定时器已经存在,则用new_expires和cb去更新该定时器。
    void add(int id, int new_expires, const TimeoutCallBack& cb);
    
    //调整定时器的触发时间
    void adjust(int id, int timeout);
    
    //删除堆顶节点
    void pop();
    
    //删除位置为index的节点。私有函数,不会被外部调用。
    void del_(int index);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 时间堆当然需要工作函数,用来触发到点的时间节点。
    • 需要一个tick心跳函数,去检查时间堆,清除所有过期节点。
    • 需要一个GetNextTick()函数,去获得下一次触发的时间,确保每次触发都会有事件就绪。
      void worker(int id);
    
      void tick();    //心跳函数
    
      int GetNextTick();
    
    • 1
    • 2
    • 3
    • 4
    • 5
  • 相关阅读:
    深入理解面向对象(第二篇)
    sstream及按格式字符分割字符串
    【笔记】电商订单数据分析实战
    Grafana系列-统一展示-6-Zabbix仪表板
    视频 | 生信Linux - 快说梳理 Linux 处理文件
    99%健身人士的疑问:营养补充窗口真的很重要吗?
    Linux系统常用的工具
    cmka下切换使用不同版本的boost-未解决
    实战Docker未授权访问提权
    【222】MyQSL技术内幕 InnoDB 存储引擎第二版 笔记
  • 原文地址:https://blog.csdn.net/qq_53558968/article/details/126061271