东阳的学习笔记
一、给 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); }