muduo笔记 网络库(四)TimerQueue定时器队列

网络编程中,有一类非常重要的事件,跟IO事件没有直接联系,而是内部产生的事件,即定时事件。

muduo网络库中的定时功能是如何实现的呢?
传统的Reactor通过控制select(2)和poll(2)的等待时间,来实现定时,而Linux中,可以用timerfd来实现。前面讲过,timerfd是Linux特有的定时器,能有效融入select/poll/epoll框架,来做超时事件处理。

参见:Linux定时器timerfd用法


timerfd简要介绍

timerfd的特点是有一个与之关联fd,可绑定Channel,交由Poller监听感兴趣的事件(读、写等)。
timerfd 3个接口: timerfd_create,timerfd_settime,timerfd_gettime。

#include <sys/timerfd.h>

/* 创建一个定时器对象, 返回与之关联的fd
* clockid 可指定为CLOCK_REALTIME(系统范围时钟)或CLOCK_MONOTONIC(不可设置的时钟,不能手动修改)
* flags 可指定为TFD_NONBLOCK(为fd设置O_NONBLOCK),TFD_CLOEXEC(为fd设置close-on-exec)
*/
int timerfd_create(int clockid, int flags);

/* 启动或停止绑定到fd的定时器
 * flags 指定0:启动一个相对定时器,由new_value->it_value指定相对定时值;TFD_TIMER_ABSTIME启动一个绝对定时器,由new_value->it_value指定定时值
 * old_value 保存旧定时值
 */
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);

/* 获取fd对应定时器的当前时间值  */
int timerfd_gettime(int fd, struct itimerspec *curr_value);

定时功能相关类

muduo定时功能如何将timerfd融入select/poll/select框架?
由3个class实现:TimerID、Timer、TimerQueue。用户可见的只有TimerId。

Timestamp类是时间戳类,用来保存超时时刻(精确到1us),保存的是UTC时间,即从 Unix Epoch(1970-01-01 00:00:00)到指定时间的微秒数。

Timer类对应一个超时任务,保存了超时时刻Timestamp,超时回调函数,以及超时任务类型(一次 or 周期)。

TimerId类用于保存Timer对象,以及独一无二的id。

TimerQueue类用于设置所有超时任务(Timer),需要高效组织尚未到期的Timer,快速查找已到期Timer,以及高效添加和删除Timer。TimerQueue用std::set存储 ,set会对Timer按到期时间先后顺序进行二叉搜索树排序,时间复杂度O(logN)。

TimerQueue的定时接口并不是直接暴露给库的使用者的,而是通过EventLoop的runAfter和runEvery来运行用户任务的。其中,runAfter延迟固定秒数后运行一次指定用户任务;runEvery延迟固定秒数后运行用户任务,后续以指定周期运行用户任务。

TimerQueue回调用户代码onTimer()的时序:
img

时序图里的TimerQueue获取超时Timer(getExpired())后,User及onTimer()是指用户自定义的超时处理函数,并非库本身的。

与普通Channel事件一样,超时任务TimerQueue也会使用一个Channel,专门用于绑定timerfd,交由Poller监听,发生可读事件(代表超时)后加入激活通道列表,然后EventLoop::loop()逐个Channel调用对应的回调,从而处理超时事件。
注意:一个EventLoop只持有一个TimerQueue对象,而TimerQueue通过std::set持有多个Timer对象,但只会设置一个Channel。


Timer类

Timer类代表一个超时任务,但并不直接绑定Channel。Timer主要包含超时时刻(expiration_),超时回调(callback_),周期时间值(interval_),全局唯一id(sequence_)。

其声明如下:

/**
* 用于定时事件的内部类
*/
class Timer : noncopyable
{
public:
    Timer(TimerCallback cb, Timestamp when, double interval)
    : callback_(std::move(cb)),
    expiration_(when),
    interval_(interval),
    repeat_(interval > 0.0),
    sequence_(s_numCreated_.incrementAndGet())
    { }
    /* 运行超时回调函数 */
    void run() const
    {
        callback_();
    }
    /* 返回超时时刻 */
    Timestamp expiration() const { return expiration_; }
    /* 周期重复标志 */
    bool repeat() const { return repeat_; }
    /* 全局唯一序列号, 用来表示当前Timer对象 */
    int64_t sequence() const { return sequence_; }
    /* 重启定时器, 只对周期Timer有效(repeat_为true) */
    void restart(Timestamp now);
    /* 当前创建的Timer对象个数, 每新建一个Timer对象就会自增1 */
    static int64_t numCreated() { return s_numCreated_.get(); }

private:
    const TimerCallback callback_; /* 超时回调 */
    Timestamp expiration_;         /* 超时时刻 */
    const double interval_;        /* 周期时间, 单位秒, 可用来结合基础时刻expiration_, 计算新的时刻 */
    const bool repeat_;            /* 重复标记. true: 周期Timer; false: 一次Timer */
    const int64_t sequence_;       /* 全局唯一序列号 */

    // global increasing number, atomic. help to identify different Timer
    static AtomicInt64 s_numCreated_; /* 类变量, 创建Timer对象的个数, 用来实现全局唯一序列号 */
};

每当创建一个新Timer对象时,原子变量s_numCreated_就会自增1,作为全剧唯一序列号sequence_,用来标识该Timer对象。

  • 周期Timer

创建Timer时,超时时刻when决定了回调超时事件时间点,而interval决定了Timer是一次性的,还是周期性的。如果是周期性的,会在TimerQueue::reset中,调用Timer::restart,在当前时间点基础上,重启定时器。

  • restart函数

restart重启Timer,根据Timer是否为周期类型,分为两种情况:
1)周期Timer,restart将重置超时时刻expiration_为当前时间 + 周期间隔时间;
2)非周期Timer,即一次性Timer,将restart将expiration_置为无效时间(默认自UTC Epoch以来的微妙数为0);

void Timer::restart(Timestamp now)
{
    if (repeat_)
    {
        expiration_ = addTime(now, interval_);
    }
    else
    {
        expiration_ = Timestamp::invalid();
    }
}

TimerId类

TimerId来主要用来作为Timer的唯一标识,用于取消(canceling)Timer。

其实现代码很简单:

/**
* An opaque identifier, for canceling Timer.
*/
class TimerId : public muduo::copyable
{
public:
    TimerId()
    : timer_(NULL),
    sequence_(0)
    { }

    TimerId(Timer* timer, int64_t seq)
    : timer_(timer),
    sequence_(seq)
    { }

    // default copy-ctor, dtor and assignment are okay

    friend class TimerQueue;

private:
    Timer* timer_;
    int64_t sequence_;
};

注意:TimerId并不直接生成Timer序列号sequence_,这是由Timer来生成的,通过构造函数传递给TimerId。而生成Timer标识的方式,在Timer类介绍中也提到过,只需要创建一个Timer对象即可,然后通过Timer::sequence()方法就可以取得该序列号。


TimerQueue类

定时器队列TimerQueue是定时功能的核心,由所在EventLoop持有,绑定一个Channel,同时维护多个定时任务(Timer)。为用户(EventLoop)提供添加定时器(addTimer)、取消定时器(cancel)接口。

同样是定时,TimerQueue与Timer有什么区别?

TimerQueue包含2个Timer集合:
1)timers_定时器集合:包含用户添加的所有Timer对象,std::set会用AVL搜索树,对集合元素按时间戳(Timestamp)从小到大顺序;
2)activeTimers_激活定时器集合:包含激活的Timer对象,与timers_包含的Timer对象相同,个数也相同,std::set会根据Timer*指针大小,对元素进行排序;3)cancelingTimers_取消定时器集合:包含所有取消的Timer对象,与activeTimers_相对。

注意:timers_和activeTimers_的类型并不相同,只是包含的Timer*相同。cancelingTimers_和activeTimers_的类型相同。

这也是TimerQueue并非Timer的原因,是一个Timer集合,根据其时间戳大小进行排序,更像是一个队列,先到期的先触发超时事件。因此,可称为Timer队列,即TimerQueue。

调用TimerQueue::addTimer的,只有EventLoop中这3个函数:

/**
* 定时功能,由用户指定绝对时间
* @details 每为定时器队列timerQueue添加一个Timer,
* timerQueue内部就会新建一个Timer对象, TimerId就保含了这个对象的唯一标识(序列号)
* @param time 时间戳对象, 单位1us
* @param cb 超时回调函数. 当前时间超过time代表时间时, EventLoop就会调用cb
* @return 一个绑定timerQueue内部新增的Timer对象的TimerId对象, 用来唯一标识该Timer对象
*/
TimerId EventLoop::runAt(Timestamp time, TimerCallback cb)
{
    return timerQueue_->addTimer(std::move(cb), time, 0.0);
}

/**
* 定时功能, 由用户相对时间, 通过runAt实现
* @param delay 相对时间, 单位s, 精度1us(小数)
* @param cb 超时回调
*/
TimerId EventLoop::runAfter(double delay, TimerCallback cb)
{
    Timestamp time(addTime(Timestamp::now(), delay));
    return runAt(time, std::move(cb));
}

/**
* 定时功能, 由用户指定周期, 重复运行
* @param interval 运行周期, 单位s, 精度1us(小数)
* @param cb 超时回调
* @return 一个绑定timerQueue内部新增的Timer对象的TimerId对象, 用来唯一标识该Timer对象
*/
TimerId EventLoop::runEvery(double interval, TimerCallback cb)
{
    Timestamp time(addTime(Timestamp::now(), interval));
    return timerQueue_->addTimer(std::move(cb), time, interval);
}

下面是TimerQueue中,3个集合相关的类型及成员定义:

    typedef std::pair<Timestamp, Timer*> Entry;
    typedef std::set<Entry> TimerList;
    typedef std::pair<Timer*, int64_t> ActiveTimer;
    typedef std::set<ActiveTimer> ActiveTimerSet;

    // Timer list sorted by expiration
    /* 用户添加的所有Timer对象集合
     * 需要为set元素比较实现operator< */
    TimerList timers_;

    // for cancel()
    ActiveTimerSet activeTimers_;
    bool callingExpiredTimers_; /* atomic */
    ActiveTimerSet cancelingTimers_;

TimerQueue声明

除了前面提到的3个集合相关类型及成员,其他成员函数和变量声明如下:

/**
* 定时器队列.
* 不能保证回调能及时调用.
*
* 只能在所在loop线程中运行, 因此线程安全是非必须的
*/
class TimerQueue : noncopyable
{
public:
    explicit TimerQueue(EventLoop* loop);
    ~TimerQueue();
    /*
     * 添加一个定时器.
     * 运行到指定时间, 调度相应的回调函数.
     * 如果interval参数 > 0.0, 就周期重复运行.
     * 必须线程安全: 可能会由其他线程调用
     */
    TimerId addTimer(TimerCallback cb, Timestamp when, double interval);
    /* 取消指定TimerId的定时器 */
    void cancel(TimerId);

private:
    ...
    void addTimerInLoop(Timer* timer);
    void cancelInLoop(TimerId timerId);
    // called when timerfd alarms
    void handleRead();
    // move out all expired timers
    std::vector<Entry> getExpired(Timestamp now);
    void reset(const std::vector<Entry>& expired, Timestamp now);

    bool insert(Timer* timer);

    EventLoop* loop_;
    const int timerfd_;
    Channel timerfdChannel_; // watch readable event of timerfd
    ...
}

TimerQueue所属EventLoop对象,通过一个EventLoop*来传递,注意这是一个raw pointer,而非smart pointer。EventLoop对象与TimerQueue对象生命周期相同,而且只会通过EventLoop对象来调用TimerQueue对象方法,因此不存在与之相关的内存泄漏或非法访问的问题。

TimerQueue构造函数

TimerQueue::TimerQueue(EventLoop *loop)
: loop_(loop),
timerfd_(createTimerfd()),
timerfdChannel_(loop, timerfd_),
timers_(),
callingExpiredTimers_(false)
{
    timerfdChannel_.setReadCallback(std::bind(&TimerQueue::handleRead, this));
    // we are always reading the timerfd, we disrm it with timerfd_settime.
    timerfdChannel_.enableReading();
}

构造TimerQueue对象时,就会绑定TimerQueue所属EventLoop,即创建TimerQueue的EventLoop对象。
另外,调用Channel::enableReading(),会将通道事件加入Poller的监听通道列表中。

交给Poller监听的timerfd,是由createTimerfd创建的:

int createTimerfd()
{
    // create timers that notify via fd
    int timerfd = ::timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
    if (timerfd < 0)
    {
        LOG_SYSFATAL << "Failed in timerfd_create";
    }
    return timerfd;
}

TimerQueue析构

析构有2点需要注意:
1)在remove绑定的通道前,要先disableAll停止监听所有通道事件;
2)timers_中Timer对象是在TimerQueue::addTimer中new出来的,需要手动delete;
另外,对注释“do not remove channel, since we're in EventLoop::dtor();”并不明白是何用意。

TimerQueue::~TimerQueue()
{
    // 关闭所有(通道)事件, Poller不再监听该通道
    timerfdChannel_.disableAll();
    // 如果正在处理该通道, 会从激活的通道列表中移除, 同时Poller不再监听该通道
    timerfdChannel_.remove();
    // 关闭通道对应timerfd
    ::close(timerfd_);

    // FIXME: I dont understand why "do not remove channel". What does it mean?
    // do not remove channel, since we're in EventLoop::dtor();

    // TimerQueue::addTimer中new出来的Timer对象, 需要手动delete
    for (const Entry& timer : timers_)
    {
        delete timer.second;
    }
}

TimerQueue重要接口

addTimer 添加定时器

注意到addTimer 会在构造一个Timer对象后,将其添加到timers_的工作转交给addTimerInLoop完成了。这是为什么?

因为调用EventLoop::runAt/runEvery的线程,可能并非TimerQueue的loop线程,而修改TimerQueue数据成员时,必须在所属loop线程中进行,因此需要通过loop_->runInLoop将工作转交给所属loop线程。
runInLoop:如果当前线程是所属loop线程,则直接运行函数;如果不是,就排队到所属loop线程末尾,等待运行。

/**
* 添加一个定时器.
* @details 运行到指定时间点when, 调度相应的回调函数cb.
* 如果interval参数 > 0.0, 就周期重复运行.
* 可能会由其他线程调用, 需要让对TimerQueue数据成员有修改的部分, 在所属loop所在线程中运行.
* @param cb 超时回调函数
* @param when 触发超时的时间点
* @param interval 循环周期. > 0.0 代表周期定时器; 否则, 代表一次性定时器
* @return 返回添加的Timer对应TimerId, 用来标识该Timer对象
*/
TimerId 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)); // 转交所属loop线程运行
    return TimerId(timer, timer->sequence());
}

/**
* 在loop线程中添加一个定时器.
* @details addTimerInLoop 必须在所属loop线程中运行
*/
void TimerQueue::addTimerInLoop(Timer *timer)
{
    loop_->assertInLoopThread();
    bool earliestChanged = insert(timer);

    if (earliestChanged)
    {
        resetTimerfd(timerfd_, timer->expiration());
    }
}

addTimerInLoop的主要工作由2个函数来完成:insert,resetTimerfd。

/**
* 插入一个timer指向的定时器
* @details timers_是std::set<std::pair<Timestamp, Timer*>>类型, 容器会自动对元素进行排序,
* 默认先按pair.first即Timestamp进行排序, 其次是pair.second(.first相同情况下才比较second),
* 这样第一个元素就是时间戳最小的元素.
* @return 定时器timer当前是否已经超时
* - true timers_为空或已经超时
* - false timers_非空, 且最近的一个定时器尚未超时
*/
bool TimerQueue::insert(Timer *timer)
{
    loop_->assertInLoopThread();
    assert(timers_.size() == activeTimers_.size());
    bool earliestChanged = false;
    Timestamp when = timer->expiration(); // 超时时刻
    TimerList::iterator it = timers_.begin();
    if (it == timers_.end() || when < it->first)
    { // 定时器集合为空 或者 新添加的timer已经超时(因为it指向的Timer超时时刻是距离当前最近的)
        earliestChanged = true; // timer已经超时
    }

    // 同时往timers_和activeTimers_集合中, 添加timer
    // 注意: timers_和activeTimers_元素类型不同, 但所包含的Timer是相同的, 个数也相同

    { // ensure insert new timer to timers_ successfully
        std::pair<TimerList::iterator, bool> result
        = timers_.insert(Entry(when, timer));
        assert(result.second); (void)result;
    }

    { // ensure insert new timer to activeTimers_ successfully
        std::pair<ActiveTimerSet::iterator, bool> result
        = activeTimers_.insert(ActiveTimer(timer, timer->sequence()));
        assert(result.second); (void)result;
    }

    assert(timers_.size() == activeTimers_.size());
    return earliestChanged;
}

cancel 取消定时器

一个已超时的定时器,会通过TimerQueue::getExpired自动清除,但一个尚未到期的定时器如何取消?
可以通过调用TimerQueue::cancel。类似于addTimer,cancel也可能在别的线程被调用,因此需要将其转交给cancelInLoop执行。

/**
* 取消一个定时器, 函数可能在别的线程调用
* @param timerId 每个定时器都有一个唯一的TimerId作为标识
*/
void TimerQueue::cancel(TimerId timerId)
{
    loop_->runInLoop(
            std::bind(&TimerQueue::cancelInLoop, this, timerId));
}

/**
* 在所属loop线程中, 取消一个定时器
* @details 同时擦出timers_, activeTimers_中包含的Timer对象, timerId用来查找该Timer对象.
* @param timerId 待取消Timer的唯一Id标识
*/
void TimerQueue::cancelInLoop(TimerId timerId)
{
    loop_->assertInLoopThread(); // 确保当前线程是所属loop线程
    assert(timers_.size() == activeTimers_.size());
    ActiveTimer timer(timerId.timer_, timerId.sequence_);
    ActiveTimerSet::const_iterator it = activeTimers_.find(timer);
    if (it != activeTimers_.end())
    {
        // 注意timers_和activeTimers_的Timer指针指向相同对象, 只能delete一次
        size_t n = timers_.erase(Entry(it->first->expiration(), it->first));
        assert(n == 1); (void)n;
        delete it->first; // FIXME: no delete please
        activeTimers_.erase(it);
    }
    else if (callingExpiredTimers_)
    { // 如果正在处理超时定时器
        cancelingTimers_.insert(timer);
    }
    assert(timers_.size() == activeTimers_.size());
}

handleRead处理TimerQueue上所有超时任务

handleRead有几个要点:
1)必须在所在loop线程运行;
2)可能不止一个定时任务超时,可用getExpired()获取;
3)所有超时任务执行完后,重置周期定时任务,释放一次性定时任务;

/**
* 处理读事件, 只能是所属loop线程调用
* @details 当PollPoller监听到超时发生时, 将channel加入激活通道列表, loop中回调
* 事件处理函数, TimerQueue::handleRead.
* 发生超时事件时, 可能会有多个超时任务超时, 需要通过getExpired一次性全部获取, 然后逐个执行回调.
* @note timerfd只会发生读事件.
*/
void TimerQueue::handleRead()
{
    loop_->assertInLoopThread();
    Timestamp now(Timestamp::now());
    readTimerfd(timerfd_, now);

    std::vector<Entry> expired = getExpired(now); // 获取所有超时任务

    // 正在调用超时任务回调时, 先清除取消的超时任务cancelingTimers_, 再逐个执行超时回调.
    // 可由getExpired()获取的所有超时任务.
    callingExpiredTimers_ = true;
    cancelingTimers_.clear();
    // safe to callback outside critical section
    for (const Entry& it : expired)
    {
        it.second->run(); // 通过Timer::run()回调超时处理函数
    }
    callingExpiredTimers_ = false;
    // 重置所有已超时任务
    reset(expired, now);
}

getExpired以参数时间点now为界限,查找set timers_中所有超时定时任务(Timer)。set会对timers_元素进行排序,std::set::lower_bound()会找到第一个时间点 < now时间点的定时任务。

getExpired调用reset重置所有超时的周期定时任务,释放超时的一次性任务。

/**
* 定时任务超时时, 从set timers_中取出所有的超时任务, 以vector形式返回给调用者
* @note 注意从set timers_要和从set activeTimers_同步取出超时任务, 两者保留的定时任务是相同的
* @param now 当前时间点, 用来判断从set中的定时器是否超时
* @return set timers_中超时的定时器
*/
std::vector<TimerQueue::Entry> TimerQueue::getExpired(Timestamp now)
{
    assert(timers_.size() == activeTimers_.size());
    std::vector<Entry> expired;
    Entry sentry(now, reinterpret_cast<Timer*>(UINTPTR_MAX));
    // end.key >= sentry.key, Entry.key is pair<Timestamp, Timer*>
    // in that end.key.second < sentry.key.second(MAX PTR)
    // => end.key == sentry.key is impossible
    // => end.key > sentry.key
    TimerList::iterator end = timers_.lower_bound(sentry);
    assert(end == timers_.end() || now < end->first);
    std::copy(timers_.begin(), end, back_inserter(expired));
    timers_.erase(timers_.begin(), end);

    for (const Entry& it : expired)
    {
        ActiveTimer timer(it.second, it.second->sequence());
        size_t n = activeTimers_.erase(timer);
        assert(n == 1); (void)n;
    }

    assert(timers_.size() == activeTimers_.size());
    return expired;
}

/**
* 根据指定时间now重置所有超时任务, 只对周期定时任务有效
* @param expired 所有超时任务
* @param now 指定的reset基准时间点, 新的超时时间点以此为基准
*/
void TimerQueue::reset(const std::vector<Entry> &expired, Timestamp now)
{
    Timestamp nextExpire;

    for (const Entry& it : expired)
    {
        ActiveTimer timer(it.second, it.second->sequence());
        // 只重置周期定时任务和没有取消的定时任务, 释放一次性超时的定时任务
        if (it.second->repeat()
        && cancelingTimers_.find(timer) == cancelingTimers_.end())
        {
            it.second->restart(now);
            insert(it.second);
        }
        else
        {
            // FIXME move to a free list
            delete it.second; // FIXME: no delete please
        }
    }

    // 根据最近的尚未达到的超时任务, 重置timerfd下一次超时时间
    if (!timers_.empty())
    {
        nextExpire = timers_.begin()->second->expiration();
    }

    if (nextExpire.valid())
    {
        resetTimerfd(timerfd_, nextExpire);
    }
}

参考

muduo网络库学习(三)定时器TimerQueue的设计 | CSDN


muduo库其它部分解析参见:muduo库笔记汇总

posted @ 2022-03-13 20:33  明明1109  阅读(1171)  评论(0编辑  收藏  举报