muduo库学习之设计与实现03——TimerQueue定时器

东阳的学习笔记

一、给 EventLoop 加上定时器功能。

  • 传统的 Reactor 通过控制 select() 和 poll() 的等待时间来实现定时
  • 现在的linux中有了 timerfd,我们可以用处理 IO 事件相同的方式来处理定时,代码的一致性更好。

1.1 timerfd

网上对于 timerfd 的文章很多,我就不总结了。见:
https://www.cnblogs.com/mickole/p/3261879.html

二、TimerQueue class

muduo 的定时器功能由三个 class 实现,TimerId、Timer、TimerQueue,用户只能看到第一个 class,另外两个都是实现细节。TimerId 和 Timer的实现很简单、这里只说TimerQueue

  • TimerQueue 的接口很简单,只有两个函数 addTimer() 和 cancel()。
class TimerQueue : boost::noncopyable
{
public:
 TimerQueue(EventLoop* loop);
 ~TimerQueue();

 ///
 /// Schedules the callback to be run at given time,
 /// repeats if @c interval > 0.0.
 ///
 /// Must be thread safe. Usually be called from other threads.
 TimerId addTimer(const TimerCallback& cb,
                  Timestamp when,
                  double interval);

 // void cancel(TimerId timerId);

2.1 TimerQueue的数据结构的选择

TimerQueue 需要:

  • 高效地组织目前尚未到期的 Timer
  • 能快速地根据当前时间找到已过期的Timer
  • 高效地增加和删除Timer
2.1.1 以按到期时间排好序的线性表为数据结构

这种结构的常用操作是线性查找,复杂度是 O(N)

2.1.2 二叉堆组织优先队列

这种做法的复杂度降为O(logN),但是C++标准库的 make_heap() 等函数不能高效地删除 heap 中间的某个元素,需要我们自己实现:

  • 令 Timer 记住自己在 heap 中的位置
2.1.3 二叉搜索树

把Timer按到期时间先后排好序。操作的复杂度仍然是O(logN),不过 memory locality 比 heap 要差一点,实际速度可能略慢。

TimerQueue 实际使用的是第二种:

  • 这个结构利用了现成的容器库,实现简单容易验证其正确性并且性能也不错
  • 使用 set 而非 map,因为只有 key 没有 value。TimerQueue使用了一个 Channel 来观察 timerfd_ 上的 readable 事件。注意 TimerQueue 的成员函数只能在其所属的 IO 线程调用,因此不必加锁
  • 这里使用的裸指针,在 C++11 之中可以使用 unique_ptr避免手动管理资源
private:

 // FIXME: use unique_ptr<Timer> instead of raw pointers.
 typedef std::pair<Timestamp, Timer*> Entry;
 typedef std::set<Entry> TimerList;

 // called when timerfd alarms
 void handleRead();
 // move out all expired timers
 std::vector<Entry> getExpired(Timestamp now);                  // 此处进行了ROV优化
 void reset(const std::vector<Entry>& expired, Timestamp now);

 bool insert(Timer* timer);

 EventLoop* loop_;
 const int timerfd_;
 Channel timerfdChannel_;
 // Timer list sorted by expiration
 TimerList timers_;
}; 

2.2 TimeQueue::getExpired()

这个函数会从 timers_ 中移除已到期的 Timer,并通过 vector 返回被移除的 Timers。

  • 编译器会实施 RVO (编译器返回值优化),不必太担心这样写的性能问题,必要时可以像 EventLoop::activateChannels_那样复用 vector。注意其中哨兵值(sentry)的选取
  • sentry 让 set::lower_bound()返回的是第一个未到期的 Timer 的迭代器,因此&6中的 assert 中是 < 而非 <=
std::vector<TimerQueue::Entry> TimerQueue::getExpired(Timestamp now)
{
 std::vector<Entry> expired;
 Entry sentry = std::make_pair(now, reinterpret_cast<Timer*>(UINTPTR_MAX));
 TimerList::iterator it = timers_.lower_bound(sentry);
 assert(it == timers_.end() || now < it->first);
 std::copy(timers_.begin(), it, back_inserter(expired));
 timers_.erase(timers_.begin(), it);

 return expired;
}

下图是 TimerQueue 回调代码 onTimer() 的时序图:
在这里插入图片描述

三、EventLoop 的改动

EventLoop新增了几个方便用户使用的定时器接口,这几个函数都转而调用 TimerQueue::addTimer()。

  • 注意这几个 EventLoop 成员函数应该允许跨线程使用,比如说我想在某个 IO 线程中执行超时回调。
  • muduo 解决由此带来的线程安全问题的方法不是加锁,而是把对 TimerQueue的操作转移到 IO 线程(Base IO Thread)来进行(EventLoop 的 IO线程只有一个,所以不会有线程安全问题)。这会用到 EventLoop::runInLoop() 函数。
TimerId EventLoop::runAt(const Timestamp& time, const TimerCallback& cb)
{
 return timerQueue_->addTimer(cb, time, 0.0);
}

TimerId EventLoop::runAfter(double delay, const TimerCallback& cb)
{
 Timestamp time(addTime(Timestamp::now(), delay));
 return runAt(time, cb);
}

TimerId EventLoop::runEvery(double interval, const TimerCallback& cb)
{
 Timestamp time(addTime(Timestamp::now(), interval));
 return timerQueue_->addTimer(cb, time, interval);
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

东阳z

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

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

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

打赏作者

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

抵扣说明:

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

余额充值