Skip to content

Latest commit

 

History

History
336 lines (304 loc) · 12.3 KB

定时器模块.md

File metadata and controls

336 lines (304 loc) · 12.3 KB

定时器模块讲解

定时器功能相关的类由 Timestamp,Timer,TimerQueue类组成,muduo 库还有一个 Timeld 类方便对定时器进行索引,本项目里没有加上这个类。

Timer

一个定时器需要哪些属性呢?

  1. 定时器到期后需要调用回调函数
  2. 我们需要让定时器记录我们设置的超时时间
  3. 如果是重复事件(比如每间隔5秒扫描一次用户连接),我们还需要记录超时时间间隔
  4. 对应的,我们需要一个 bool 类型的值标注这个定时器是一次性的还是重复的
class Timer : noncopyable
{
private:
    const TimerCallback callback_;  // 定时器回调函数
    Timestamp expiration_;          // 下一次的超时时刻
    const double interval_;         // 超时时间间隔,如果是一次性定时器,该值为0
    const bool repeat_;             // 是否重复(false 表示是一次性定时器)
};

其他的方法比较简单,基本是一些定时器属性的返回。

class Timer : noncopyable
{
public:
    using TimerCallback = std::function<void()>;
    // 构造函数
    Timer(TimerCallback cb, Timestamp when, double interval)
        : callback_(move(cb)),
          expiration_(when),
          interval_(interval),
          repeat_(interval > 0.0) // 一次性定时器设置为0
    {
    }
    // 调用此定时器的回调函数
    void run() const 
    { 
        callback_(); 
    }
    // 返回此定时器超时时间
    Timestamp expiration() const  { return expiration_; }
    bool repeat() const { return repeat_; }

    // 重启定时器(如果是非重复事件则到期时间置为0)
    void restart(Timestamp now);
};

这里稍微介绍以下 restart 方法 观察定时器构造函数的repeat_(interval > 0.0),这里会根据「间隔时间是否大于0.0」来判断此定时器是重复使用的还是一次性的。 如果是重复使用的定时器,在触发任务之后还需重新利用。这里就会调用 restart 方法。我们设置其下一次超时时间为「当前时间 + 间隔时间」。如果是「一次性定时器」,那么就会自动生成一个空的 Timestamp,其时间自动设置为 0.0

void Timer::restart(Timestamp now)
{
    if (repeat_)
    {
        // 如果是重复定时事件,则继续添加定时事件,得到新事件到期事件
        expiration_ = addTime(now, interval_);
    }
    else 
    {
        expiration_ = Timestamp();
    }
}

TimerQueue

TimerQueue类管理作为管理定时器的结构。其内部使用 STL 容器 set 来管理定时器。我们以时间戳作为键值来获取定时器。set 内部实现是红黑树,红黑树中序遍历便可以得到按照键值排序过后的定时器节点。

using Entry = std::pair<Timestamp, Timer*>;
using TimerList = std::set<Entry>;

TimerQueue类方法和成员变量

class TimerQueue
{
public:
    using TimerCallback = std::function<void()>;

    explicit TimerQueue(EventLoop* loop);
    ~TimerQueue();

    // 插入定时器(回调函数,到期时间,是否重复)
    void addTimer(TimerCallback cb,
                  Timestamp when,
                  double interval);
    
private:
    using Entry = std::pair<Timestamp, Timer*>;
    using TimerList = std::set<Entry>;

    // 在本loop中添加定时器
    // 线程安全
    void addTimerInLoop(Timer* timer);

    // 定时器读事件触发的函数
    void handleRead();

    // 重新设置timerfd_
    void resetTimerfd(int timerfd_, Timestamp expiration);
    
    // 移除所有已到期的定时器
    // 1.获取到期的定时器
    // 2.重置这些定时器(销毁或者重复定时任务)
    std::vector<Entry> getExpired(Timestamp now);
    void reset(const std::vector<Entry>& expired, Timestamp now);

    // 插入定时器的内部方法
    bool insert(Timer* timer);

    EventLoop* loop_; // 所属的EventLoop
    const int timerfd_; // timerfd是Linux提供的定时器接口
    Channel timerfdChannel_; // 封装timerfd_文件描述符
    // Timer list sorted by expiration
    TimerList timers_; // 定时器队列(内部实现是红黑树)

    bool callingExpiredTimers_; // 正在获取超时定时器标志
};

使用timerfd实现定时功能

linux2.6.25 版本新增了timerfd这个供用户程序使用的定时接口,这个接口基于文件描述符,当超时事件发生时,该文件描述符就变为可读。这种特性可以使我们在写服务器程序时,很方便的便把定时事件变成和其他I/O事件一样的处理方式,当时间到期后,就会触发读事件。我们调用响应的回调函数即可。

timer_create

int timer_create(int clockid,int flags); // 成功返回0

此函数用于创建timerfd,我们需要指明使用标准事件还是相对事件,并且传入控制标志。

  • CLOCK_REALTIME:相对时间,从1970.1.1到目前时间,之所以说其为相对时间,是因为我们只要改变当前系统的时间,从1970.1.1到当前时间就会发生变化,所以说其为相对时间
  • CLOCK_MONOTONIC:绝对时间,获取的时间为系统最近一次重启到现在的时间,更该系统时间对其没影响
  • flag:TFD_NONBLOCK(非阻塞),TFD_CLOEXEC

timerfd_settime

int timerfd_settime(int fd,int flags
                    const struct itimerspec *new_value
                    struct itimerspec *old_value);
                    //成功返回0

我们使用此函数启动或停止定时器。

  • fd:timerfd_create()返回的文件描述符
  • flags:0表示相对定时器,TFD_TIMER_ABSTIME表示绝对定时器
  • new_value:设置超时事件,设置为0则表示停止定时器
  • old_value:原来的超时时间,不使用可以置为NULL

创建TimerQueue

  1. 通过timer_create创建timerfd
  2. TimerQueue类初始化后,设置其timerfdChannel绑定读事件,并置于可读状态
int createTimerfd()
{
    /**
     * CLOCK_MONOTONIC:绝对时间
     * TFD_NONBLOCK:非阻塞
     */
    int timerfd = ::timerfd_create(CLOCK_MONOTONIC,
                                    TFD_NONBLOCK | TFD_CLOEXEC);
    if (timerfd < 0)
    {
        LOG_ERROR("Failed in timerfd_create");
    }
    return timerfd;
}

TimerQueue::TimerQueue(EventLoop* loop)
    : loop_(loop),
      timerfd_(createTimerfd()),
      timerfdChannel_(loop_, timerfd_),
      timers_()
{
    timerfdChannel_.setReadCallback(
        std::bind(&TimerQueue::handleRead, this));
    timerfdChannel_.enableReading();
}

删除定时器管理对象

TimerQueue::~TimerQueue()
{   
    timerfdChannel_.disableAll();
    timerfdChannel_.remove();
    ::close(timerfd_);
    // 删除所有定时器
    for (const Entry& timer : timers_)
    {
        delete timer.second;
    }
}

插入定时器流程

  1. EventLoop调用方法,加入一个定时器事件,会向里传入定时器回调函数,超时时间和间隔时间(为0.0则为一次性定时器),addTimer方法根据这些属性构造新的定时器。
  2. 定时器队列内部插入此定时器,并判断这个定时器的超时时间是否比先前的都早。如果是最早触发的,就会调用resetTimerfd方法重新设置tiemrfd_的触发时间。内部会根据超时时间和现在时间计算出新的超时时间。
void TimerQueue::addTimer(TimerCallback cb,
                          Timestamp when,
                          double interval)
{
    Timer* timer = new Timer(std::move(cb), when, interval);
    loop_->runInLoop(
        std::bind(&TimerQueue::addTimerInLoop, this, timer));
}

void TimerQueue::addTimerInLoop(Timer* timer)
{
    // 是否取代了最早的定时触发时间
    bool eraliestChanged = insert(timer);

    // 我们需要重新设置timerfd_触发时间
    if (eraliestChanged)
    {
        resetTimerfd(timerfd_, timer->expiration());
    }
}

内部实现的插入方法获取此定时器的超时时间,如果比先前的时间小就说明第一个触发。那么我们会设置好布尔变量。因此这涉及到timerfd_的触发时间。

bool TimerQueue::insert(Timer* timer)
{
    bool earliestChanged = false;
    // 获取超时时间
    Timestamp when = timer->expiration();
    TimerList::iterator it = timers_.begin();
    if (it == timers_.end() || when < it->first)
    {
        // 说明最早的定时器已经被替换了
        earliestChanged = true;
    }

    // 管理定时器的红黑树插入此新节点
    timers_.insert(Entry(when, timer));

    return earliestChanged;
}

重置timerfd_

// 重置timerfd
void TimerQueue::resetTimerfd(int timerfd_, Timestamp expiration)
{
    struct itimerspec newValue;
    struct itimerspec oldValue;
    memset(&newValue, '\0', sizeof(newValue));
    memset(&oldValue, '\0', sizeof(oldValue));

    // 剩下时间 = 超时时间 - 当前时间
    int64_t microSecondDif = expiration.microSecondsSinceEpoch() - Timestamp::now().microSecondsSinceEpoch();
    if (microSecondDif < 100)
    {
        microSecondDif = 100;
    }

    struct timespec ts;
    ts.tv_sec = static_cast<time_t>(
        microSecondDif / Timestamp::kMicroSecondsPerSecond);
    ts.tv_nsec = static_cast<long>(
        (microSecondDif % Timestamp::kMicroSecondsPerSecond) * 1000);
    newValue.it_value = ts;
    // 此函数会唤醒事件循环
    if (::timerfd_settime(timerfd_, 0, &newValue, &oldValue))
    {
        LOG_ERROR("timerfd_settime faield()");
    }
}

处理到期定时器

  1. EventLoop获取活跃的activeChannel,并分别调取它们绑定的回调函数。这里对于timerfd_,就是调用了handleRead方法。
  2. handleRead方法获取已经超时的定时器组数组,遍历到期的定时器并调用内部绑定的回调函数。之后调用reset方法重新设置定时器
  3. reset方法内部判断这些定时器是否是可重复使用的,如果是则继续插入定时器管理队列,之后自然会触发事件。如果不是,那么就删除。如果重新插入了定时器,记得还需重置timerfd_。
void TimerQueue::handleRead()
{
    Timestamp now = Timestamp::now();
    ReadTimerFd(timerfd_);

    std::vector<Entry> expired = getExpired(now);

    // 遍历到期的定时器,调用回调函数
    callingExpiredTimers_ = true;
    for (const Entry& it : expired)
    {
        it.second->run();
    }
    callingExpiredTimers_ = false;
    
    // 重新设置这些定时器
    reset(expired, now);
}
// 返回删除的定时器节点 (std::vector<Entry> expired)
std::vector<TimerQueue::Entry> TimerQueue::getExpired(Timestamp now)
{
    std::vector<Entry> expired;
    // TODO:???
    Entry sentry(now, reinterpret_cast<Timer*>(UINTPTR_MAX));
    TimerList::iterator end = timers_.lower_bound(sentry);
    std::copy(timers_.begin(), end, back_inserter(expired));
    timers_.erase(timers_.begin(), end);
    
    return expired;
}
void TimerQueue::reset(const std::vector<Entry>& expired, Timestamp now)
{
    Timestamp nextExpire;
    for (const Entry& it : expired)
    {
        // 重复任务则继续执行
        if (it.second->repeat())
        {
            auto timer = it.second;
            timer->restart(Timestamp::now());
            insert(timer);
        }
        else
        {
            delete it.second;
        }

        // 如果重新插入了定时器,需要继续重置timerfd
        if (!timers_.empty())
        {
            resetTimerfd(timerfd_, (timers_.begin()->second)->expiration());
        }
    }
}

muduo中定时器模块的特点

  1. 整个TimerQueue只使用一个timerfd来观察定时事件,并且每次重置timerfd时只需跟set中第一个节点比较即可,因为set本身是一个有序队列。
  2. 整个定时器队列采用了muduo典型的事件分发机制,可以使的定时器的到期时间像fd一样在Loop线程中处理。
  3. 之前Timestamp用于比较大小的重载方法在这里得到了很好的应用

参考

muduo中的定时器-ZWiley的随记 linux新增特性timerfd_Shreck66的博客-CSDN博客