• 【服务器学习】timer定时器模块


    timer定时器模块

    以下是从sylar服务器中学的,对其的复习;
    参考资料

    定时器概述

    通过定时器可以实现给服务器注册定时事件,这是服务器上经常要处理的一类事件,比如3秒后关闭一个连接,或是定期检测一个客户端的连接状态。
    定时事件依赖于Linux提供的定时机制,它是驱动定时事件的原动力,目前Linux提供了以下几种可供程序利用的定时机制:

    1. alarm()或setitimer(),这俩的本质都是先设置一个超时时间,然后等SIGALARM信号触发,通过捕获信号来判断超时
    2. 套接字超时选项,对应SO_RECVTIMEO和SO_SNDTIMEO,通过errno来判断超时
    3. 多路复用超时参数,select/poll/epoll都支持设置超时参数,通过判断返回值为0来判断超时
    4. timer_create系统接口,实质也是借助信号,参考man 2 timer_create
    5. timerfd_create系列接口,通过判断文件描述符可读来判断超时,可配合IO多路复用
    • sylar使用时间堆的方式管理定时器

    每次都取出所有定时器中超时时间最小的超时值作为一个tick,这样,一旦tick触发,超时时间最小的定时器必然到期。处理完已超时的定时器后,再从剩余的定时器中找出超时时间最小的一个,并将这个最小时间作为下一个tick,如此反复,就可以实现较为精确的定时。
    最小堆很适合处理这种定时方案,将所有定时器按最小堆来组织,可以很方便地获取到当前的最小超时时间,sylar采取的即是这种方案。

    sylar定时器设计

    sylar的定时器采用最小堆设计,所有定时器根据绝对的超时时间点进行排序,每次取出离当前时间最近的一个超时时间点,计算出超时需要等待的时间,然后等待超时。超时时间到后,获取当前的绝对时间点,然后把最小堆里超时时间点小于这个时间点的定时器都收集起来,执行它们的回调函数。

    关于定时器和IO协程调度器的整合。IO协程调度器的idle协程会在调度器空闲时阻塞在epoll_wait上,等待IO事件发生。在之前的代码里,epoll_wait具有固定的超时时间,这个值是5秒钟。加入定时器功能后,epoll_wait的超时时间改用当前定时器的最小超时时间来代替。epoll_wait返回后,根据当前的绝对时间把已超时的所有定时器收集起来,执行它们的回调函数。

    由于epoll_wait的返回并不一定是超时引起的,也有可能是IO事件唤醒的,所以在epoll_wait返回后不能想当然地假设定时器已经超时了,而是要再判断一下定时器有没有超时,这时绝对时间的好处就体现出来了,通过比较当前的绝对时间和定时器的绝对超时时间,就可以确定一个定时器到底有没有超时。

    sylar定时器实现

    sylar的定时器对应Timer类,这个类的成员变量包括定时器的绝对超时时间点,是否重复执行,回调函数,以及一个指向TimerManager的指针,提供cancel/reset/refresh方法用于操作定时器。构造Timer时可以传入超时时间,也可以直接传入一个绝对时间。Timer的构造函数被定义成私有方式,只能通过TimerManager类来创建Timer对象。除此外,Timer类还提供了一个仿函数Comparator,用于比较两个Timer对象,比较的依据是绝对超时时间。

    class TimerManager;
    /**
     * @brief 定时器
     */
    class Timer : public std::enable_shared_from_this<Timer> {
    friend class TimerManager;
    public:
        /// 定时器的智能指针类型
        typedef std::shared_ptr<Timer> ptr;
     
        /**
         * @brief 取消定时器
         */
        bool cancel();
     
        /**
         * @brief 刷新设置定时器的执行时间
         */
        bool refresh();
     
        /**
         * @brief 重置定时器时间
         * @param[in] ms 定时器执行间隔时间(毫秒)
         * @param[in] from_now 是否从当前时间开始计算
         */
        bool reset(uint64_t ms, bool from_now);
    private:
        /**
         * @brief 构造函数
         * @param[in] ms 定时器执行间隔时间
         * @param[in] cb 回调函数
         * @param[in] recurring 是否循环
         * @param[in] manager 定时器管理器
         */
        Timer(uint64_t ms, std::function<void()> cb,
              bool recurring, TimerManager* manager);
        /**
         * @brief 构造函数
         * @param[in] next 执行的时间戳(毫秒)
         */
        Timer(uint64_t next);
    private:
        /// 是否循环定时器
        bool m_recurring = false;
        /// 执行周期
        uint64_t m_ms = 0;
        /// 精确的执行时间
        uint64_t m_next = 0;
        /// 回调函数
        std::function<void()> m_cb;
        /// 定时器管理器
        TimerManager* m_manager = nullptr;
    private:
        /**
         * @brief 定时器比较仿函数
         */
        struct Comparator {
            /**
             * @brief 比较定时器的智能指针的大小(按执行时间排序)
             * @param[in] lhs 定时器智能指针
             * @param[in] rhs 定时器智能指针
             */
            bool operator()(const Timer::ptr& lhs, const Timer::ptr& rhs) const;
        };
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    所有的Timer对象都由TimerManager类进行管理,TimerManager包含一个std::set类型的Timer集合,这个集合就是定时器的最小堆结构,因为set里的元素总是排序过的,所以总是可以很方便地获取到当前的最小定时器。TimerManager提供创建定时器,获取最近一个定时器的超时时间,以及获取全部已经超时的定时器回调函数的方法,并且提供了一个onTimerInsertedAtFront()方法,这是一个虚函数,由IOManager继承时实现,当新的定时器插入到Timer集合的首部时,TimerManager通过该方法来通知IOManager立刻更新当前的epoll_wait超时。TimerManager还负责检测是否发生了校时,由detectClockRollover方法实现。

    /**
     * @brief 定时器管理器
     */
    class TimerManager {
    friend class Timer;
    public:
        /// 读写锁类型
        typedef RWMutex RWMutexType;
     
        /**
         * @brief 构造函数
         */
        TimerManager();
     
        /**
         * @brief 析构函数
         */
        virtual ~TimerManager();
     
        /**
         * @brief 添加定时器
         * @param[in] ms 定时器执行间隔时间
         * @param[in] cb 定时器回调函数
         * @param[in] recurring 是否循环定时器
         */
        Timer::ptr addTimer(uint64_t ms, std::function<void()> cb
                            ,bool recurring = false);
     
        /**
         * @brief 添加条件定时器
         * @param[in] ms 定时器执行间隔时间
         * @param[in] cb 定时器回调函数
         * @param[in] weak_cond 条件
         * @param[in] recurring 是否循环
         */
        Timer::ptr addConditionTimer(uint64_t ms, std::function<void()> cb
                            ,std::weak_ptr<void> weak_cond
                            ,bool recurring = false);
     
        /**
         * @brief 到最近一个定时器执行的时间间隔(毫秒)
         */
        uint64_t getNextTimer();
     
        /**
         * @brief 获取需要执行的定时器的回调函数列表
         * @param[out] cbs 回调函数数组
         */
        void listExpiredCb(std::vector<std::function<void()> >& cbs);
     
        /**
         * @brief 是否有定时器
         */
        bool hasTimer();
    protected:
     
        /**
         * @brief 当有新的定时器插入到定时器的首部,执行该函数
         */
        virtual void onTimerInsertedAtFront() = 0;
     
        /**
         * @brief 将定时器添加到管理器中
         */
        void addTimer(Timer::ptr val, RWMutexType::WriteLock& lock);
    private:
        /**
         * @brief 检测服务器时间是否被调后了
         */
        bool detectClockRollover(uint64_t now_ms);
    private:
        /// Mutex
        RWMutexType m_mutex;
        /// 定时器集合
        std::set<Timer::ptr, Timer::Comparator> m_timers;
        /// 是否触发onTimerInsertedAtFront
        bool m_tickled = false;
        /// 上次执行时间
        uint64_t m_previouseTime = 0;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    IOManager通过继承的方式获得TimerManager类的所有方法,这种方式相当于给IOManager外挂了一个定时器管理模块。为支持定时器功能,需要重新改造idle协程的实现,epoll_wait应该根据下一个定时器的超时时间来设置超时参数。

    class IOManager : public Scheduler, public TimerManager {
    ...
    }
     
    void IOManager::idle() {
        SYLAR_LOG_DEBUG(g_logger) << "idle";
     
        // 一次epoll_wait最多检测256个就绪事件,如果就绪事件超过了这个数,那么会在下轮epoll_wati继续处理
        const uint64_t MAX_EVNETS = 256;
        epoll_event *events       = new epoll_event[MAX_EVNETS]();
        std::shared_ptr<epoll_event> shared_events(events, [](epoll_event *ptr) {
            delete[] ptr;
        });
     
        while (true) {
            // 获取下一个定时器的超时时间,顺便判断调度器是否停止
            uint64_t next_timeout = 0;
            if( SYLAR_UNLIKELY(stopping(next_timeout))) {
                SYLAR_LOG_DEBUG(g_logger) << "name=" << getName() << "idle stopping exit";
                break;
            }
     
            // 阻塞在epoll_wait上,等待事件发生或定时器超时
            int rt = 0;
            do{
                // 默认超时时间5秒,如果下一个定时器的超时时间大于5秒,仍以5秒来计算超时,避免定时器超时时间太大时,epoll_wait一直阻塞
                static const int MAX_TIMEOUT = 5000;
                if(next_timeout != ~0ull) {
                    next_timeout = std::min((int)next_timeout, MAX_TIMEOUT);
                } else {
                    next_timeout = MAX_TIMEOUT;
                }
                rt = epoll_wait(m_epfd, events, MAX_EVNETS, (int)next_timeout);
                if(rt < 0 && errno == EINTR) {
                    continue;
                } else {
                    break;
                }
            } while(true);
     
            // 收集所有已超时的定时器,执行回调函数
            std::vector<std::function<void()>> cbs;
            listExpiredCb(cbs);
            if(!cbs.empty()) {
                for(const auto &cb : cbs) {
                    schedule(cb);
                }
                cbs.clear();
            }
            ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

  • 相关阅读:
    Elasticsearch 如何实现时间差查询?
    JColorChooser 和JFileChooser
    三丁基-巯基膦烷「tBuBrettPhos Pd(allyl)」OTf),1798782-17-8
    华为19级专家10年心血终成百页负载均衡高并发网关设计实战文档
    ssh服务登录原理与配置
    Java基础复习 Day 23
    体系结构30_同步性能问题
    蓝桥杯2023年第十四届省赛真题-异或和之和--题解
    白嫖阿里云服务器教程来了,薅秃阿里云!
    10000m3d城镇生活污水处理工艺设计
  • 原文地址:https://blog.csdn.net/qq_60755751/article/details/134482552