【C++模块实现】| 【10】定时器的实现

索引

【C++模块实现】| 【01】日志系统实现
【C++模块实现】| 【02】日志系统优化
【C++模块实现】| 【03】文件管理模块
【C++模块实现】| 【04】配置模块
【C++模块实现】| 【05】日志模块增加配置模块的功能
【C++模块实现】| 【06】日志模块添加循环覆盖写文件功能
【C++模块实现】| 【07】对于互斥、自旋锁、条件变量、信号量简介及封装
【C++模块实现】| 【08】循环覆盖写内存缓冲区(日志中多线程记录)
【C++模块实现】| 【09】线程模块及线程池的实现
【C++模块实现】| 【10】定时器的实现
【C++模块实现】| 【11】封装Ipv4、Ipv6、unix网络地址

1 简介

一般用于做定时任务,设置几秒钟后执行某个函数;

在服务端中,与时间有关的常见任务:

  • 获取当前时间,计算时间间隔;
  • 时区转换与日期计算;
  • 定时操作;

1.1 设计定时器需要考虑的问题

  • 如何选用正确(效率高,精度高)的时间函数及结构体;
  • 给定时器挑选合适的数据结构;
  • 定时器成员:一般包括回调任务函数,执行时,是否周期执行,是否自启动;

2 Linux时间函数的选择

【获取当前时间】

  • time(time_t):(秒)精度较低;
  • ftime/struct timeb:(毫秒)被弃用;
  • gettimeofday/struct timeval:(微秒)一般选用该函数,精度够,且不为系统调用;
  • clock_gettime/struct timespec:(纳秒),精度高,但是系统调用开销较大;

【时间格式转换函数】

  • struct tm *gmtime(const time_t *timep)
  • struct tm *localtime(const time_t *timep)
  • time_t timegm(struct tm *tm)
  • time_t mktime(struct tm *tm)
  • size_t strftime(char *s, size_t max, const char *format, const struct tm *tm)
  • struct tm

【定时函数】

  • sleep:可能使用信号,但在多线程下应尽量避免使用;
  • alarm:可能使用信号,但在多线程下应尽量避免使用;
  • usleep:可能使用信号,但在多线程下应尽量避免使用;
  • nanosleep:是线程安全,但该函数是将程序挂起,一般不适用;
  • clock_nanosleep:是线程安全,但该函数是将程序挂起,一般不适用;
  • getitimer/setitimer:可能使用信号,但在多线程下应尽量避免使用;
  • timer_create/timer_settime/timer_gettime/timer_delete:可能使用信号,但在多线程下应尽量避免使用;
  • timerfd_create/timefd_gettime/timerfd_settime:将时间变成文件描述符,该文件在定时器超时时为可读,便于融入select/poll/epoll使用;
    • 但poll和epoll_wait精度只有毫秒,远低于timerfd_settime的精度;

3 定时器结构

3.1 链表

即通过链表(已序)来保存定时器;

  • 获取即O(1),链表头部为最早超时任务;
  • 插入O(n),需要遍历整个链表;

在这里插入图片描述

3.2 最小堆

即通过最小堆来保存定时器;

  • 获取最小定时器的时间复杂度为 O(1),即堆顶;
  • 插入的时间复杂度为 O(log n);
    在这里插入图片描述

3.3 红黑树

即使用平衡二叉树(如红黑树)保存定时器;

  • 获取最小定时器的时间复杂度为 O(log n)【注】也可以通过缓存最小值的方法来达到O(1);
  • 插入的时间复杂度为O(log n);
    在这里插入图片描述

3.4 时间轮

对于定时器依赖性比较高场景,以上的数据结构不能满足要求;使用高效的定时器算法:时间轮;

  • 插入、删除、轮询的时间复杂度都为O(1);
  • 时间轮类似时钟,没转动一格就会指向下一个时间槽,具体的时间滴答却决于具体实现;
  • 若没转动一个即需要1秒,则转完一圈需要N秒,则第N秒的定时器需要加到第N槽中;
    在这里插入图片描述

【公式】

  • 【插入】转动一个槽时间为si,若有N个槽,起始时间为ts,时间为t的需要加入定时器:((t-ts) / si) % N
  • 【轮询】时间轮检查定时器是否时间到,若当前时间为tc:((tc - ts) / si) % N

【分层时间轮】

  • 对时、分、秒进行分层,先将任务存放在时时间轮,当到达时时在移动到分时间轮上,到达分时在移动在对应的秒时间轮上;
    在这里插入图片描述

4 定时器处理

固定周期

  • 程序维护一个周期性tick信号,利用上述的定时函数,定期检查定时器是否超时;

直接拿超时时间当tick周期

  • 每次取出定时器中最小的超时时间作为tick,即tick触发时,最小定时器必然到期,即可处理,当处理完后,在获取下一个tick,即可实现精确定时;
  • 该方式最适合用最小堆来处理;

5 sylar定时器设计

5.1 Timer类

主要数据成员有:

  • m_recurring:是否需要周期执行,为布尔类型;
  • m_ms:执行时间周期;
  • m_next:精确的执行时间即=当前时间+m_ms;
  • m_cb:任务回调函数;
  • m_manager:所属的定时器管理器;

主要成员函数:

  • cancel:取消定时器,从管理器中删除;
  • refresh:刷新设置定时器的执行时间,即m_next=重新获取当前时间+m_ms;
  • reset:重置定时器事件,可重新设定以当前时间为起点,也可只修改执行周期时间;

5.2 TimerManager类

主要数据成员:

  • 锁:在多线程下保护好定时器集合;
  • m_timers:定时器集合;
  • m_previouseTime:上次执行时间;

主要成员函数:

  • addTimer:添加定时器,并返回定时器可供用户操作;
  • OnTimer:使用弱回调来执行定时任务;
  • addConditionTimer:添加定时器绑定一个变量,该定时器需要在特定条件下执行,当该条件消失时,即失效;
  • getNextTimer:获取下一个定时器的执行时间;
  • listExpiredCb:获取已超时的定时器执行函数;
  • onTimerInsertedAtFront:用来检测插入的定时器是否时间最近,若是,则需要tickle更新一下epoll_wait超时时间;
  • detectClockRollover:检测是否发生较时;

总述

  • 该定时器是基于epoll来实现的,时间精度只支持到毫秒级;
  • Timer可直接传入绝对执行时间来创建或传入绝对执行时间加执行函数并设定是否需要执行周期;
    • 绝对执行时间能够有效避免系统时间不精确;
    • 所有定时器只能通过TimerManager来创建管理;
    • 创建的同时能够返回相应的定时器,供用户操作;
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;
    };
};

/**
 * @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;
};


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();
        }
        ...
}
  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jxiepc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值