# 定时器模块讲解

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

## Timer
一个定时器需要哪些属性呢？

1. 定时器到期后需要调用回调函数
2. 我们需要让定时器记录我们设置的超时时间
3. 如果是重复事件（比如每间隔5秒扫描一次用户连接），我们还需要记录超时时间间隔
4. 对应的，我们需要一个 bool 类型的值标注这个定时器是一次性的还是重复的
```cpp
class Timer : noncopyable
{
private:
    const TimerCallback callback_;  // 定时器回调函数
    Timestamp expiration_;          // 下一次的超时时刻
    const double interval_;         // 超时时间间隔，如果是一次性定时器，该值为0
    const bool repeat_;             // 是否重复(false 表示是一次性定时器)
};
```
其他的方法比较简单，基本是一些定时器属性的返回。
```cpp
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`。
```cpp
void Timer::restart(Timestamp now)
{
    if (repeat_)
    {
        // 如果是重复定时事件，则继续添加定时事件，得到新事件到期事件
        expiration_ = addTime(now, interval_);
    }
    else 
    {
        expiration_ = Timestamp();
    }
}
```
## TimerQueue
TimerQueue类管理作为管理定时器的结构。其内部使用 STL 容器 set 来管理定时器。我们以时间戳作为键值来获取定时器。set 内部实现是红黑树，红黑树中序遍历便可以得到按照键值排序过后的定时器节点。
```cpp
using Entry = std::pair<Timestamp, Timer*>;
using TimerList = std::set<Entry>;
```
TimerQueue类方法和成员变量
```cpp
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
```cpp
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
```cpp
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绑定读事件，并置于可读状态
```cpp
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();
}
```
### 删除定时器管理对象
```cpp
TimerQueue::~TimerQueue()
{   
    timerfdChannel_.disableAll();
    timerfdChannel_.remove();
    ::close(timerfd_);
    // 删除所有定时器
    for (const Entry& timer : timers_)
    {
        delete timer.second;
    }
}
```
### 插入定时器流程

1. EventLoop调用方法，加入一个定时器事件，会向里传入定时器回调函数，超时时间和间隔时间（为0.0则为一次性定时器），`addTimer`方法根据这些属性构造新的定时器。
2. 定时器队列内部插入此定时器，并判断这个定时器的超时时间是否比先前的都早。如果是最早触发的，就会调用`resetTimerfd`方法重新设置tiemrfd_的触发时间。内部会根据超时时间和现在时间计算出新的超时时间。
```cpp
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_的触发时间。
```cpp
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_
```cpp
// 重置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_。
```cpp
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);
}
```
```cpp
// 返回删除的定时器节点 （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;
}
```
```cpp
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的随记](https://zwiley.github.io/mybook/Muduo/4%20muduo%E4%B8%AD%E7%9A%84%E5%AE%9A%E6%97%B6%E5%99%A8/)
[linux新增特性timerfd_Shreck66的博客-CSDN博客](https://blog.csdn.net/Shreck66/article/details/49745149?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166159041916782248565318%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=166159041916782248565318&biz_id=0&spm=1018.2226.3001.4187)
