本文最后更新于:8 个月前
WebServer(6) 定时器模块
写在前面
定时器功能相关的类由 Timestamp,Timer,TimerQueue类组成,muduo 库还有一个 Timeld 类方便对定时器进行索引,本项目里没有加上这个类。
正文
1 Timer类
一个定时器需要哪些属性呢?
- 定时器到期后需要调用回调函数
- 我们需要让定时器记录我们设置的超时时间
- 如果是重复事件(比如每间隔5秒扫描一次用户连接),我们还需要记录超时时间间隔
- 对应的,我们需要一个 bool 类型的值标注这个定时器是一次性的还是重复的
| class Timer : noncopyable {{ private: const TimerCallback callback_; Timestamp expiration_; const double interval_; const bool repeat_; };
|
其他的方法比较简单,基本是一些定时器属性的返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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) { } void Run() const { callback_(); } Timestamp Expiration() const { return expiration_; } bool Repeat() const { return repeat_; } void Restart(Timestamp now); };
|
这里稍微介绍一下 Restart
方法
观察定时器构造函数的repeat_(interval > 0.0)
,这里会根据「间隔时间是否大于0.0」来判断此定时器是重复使用的还是一次性的。
如果是重复使用的定时器,在触发任务之后还需重新利用。这里就会调用 Restart
方法。我们设置其下一次超时时间为「当前时间 + 间隔时间」。如果是「一次性定时器」,那么就会自动生成一个空的 Timestamp,其时间自动设置为 0.0
。
1 2 3 4 5 6 7 8
| void Timer::Restart(Timestamp now) { if (repeat_) { expiration_ = AddTime(now, interval_); } else { expiration_ = Timestamp(); } }
|
2 TimerQueue类
TimerQueue类管理作为管理定时器的结构。其内部使用 STL 容器 set 来管理定时器。我们以时间戳作为键值来获取定时器。set 内部实现是红黑树,红黑树中序遍历便可以得到按照键值排序过后的定时器节点。
1 2
| using Entry = std::pair<Timestamp, Timer*>; using TimerList = std::set<Entry>;
|
TimerQueue类方法和成员变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| 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>;
void AddTimerInLoop(Timer* timer);
void HandleRead();
void ResetTimerfd(int timerfd_, Timestamp expiration); 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_; TimerList timers_;
bool callingExpiredTimers_; };
|
2.1 使用timerfd实现定时功能
linux2.6.25 版本新增了timerfd这个供用户程序使用的定时接口,这个接口基于文件描述符,当超时事件发生时,该文件描述符就变为可读。这种特性可以使我们在写服务器程序时,很方便的便把定时事件变成和其他I/O事件一样的处理方式,当时间到期后,就会触发读事件。我们调用响应的回调函数即可。
2.1.1 timer_create
1
| int timer_create(int clockid,int flags);
|
此函数用于创建timerfd,我们需要指明使用绝对时间还是相对时间,并且传入控制标志。
- CLOCK_REALTIME:相对时间,从1970.1.1到目前时间,之所以说其为相对时间,是因为我们只要改变当前系统的时间,从1970.1.1到当前时间就会发生变化,所以说其为相对时间
- CLOCK_MONOTONIC:绝对时间,获取的时间为系统最近一次重启到现在的时间,更该系统时间对其没影响
- flag:TFD_NONBLOCK(非阻塞),TFD_CLOEXEC
2.1.2 timerfd_settime
1 2 3 4
| int timerfd_settime(int fd,int flags const struct itimerspec *new_value struct itimerspec *old_value);
|
我们使用此函数启动或停止定时器。
- fd:
timerfd_create()
返回的文件描述符
- flags:0表示相对定时器,
TFD_TIMER_ABSTIME
表示绝对定时器
- new_value:设置超时时间,设置为0则表示停止定时器
- old_value:原来的超时时间,不使用可以置为NULL
2.2 创建TimerQueue
- 通过timer_create创建timerfd
- TimerQueue类初始化后,设置其timerfdChannel绑定读事件,并置于可读状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int CreateTimerfd() {
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(); }
|
2.3 删除定时器管理对象
1 2 3 4 5 6 7 8 9
| TimerQueue::~TimerQueue() { timerfdChannel_.DisableAll(); timerfdChannel_.Remove(); ::close(timerfd_); for (const Entry& timer : timers_) { delete timer.second; } }
|
2.4 插入定时器流程
- EventLoop调用方法,加入一个定时器事件,会向里传入定时器回调函数,超时时间和间隔时间(为0.0则为一次性定时器),
AddTimer
方法根据这些属性构造新的定时器。
- 定时器队列内部插入此定时器,并判断这个定时器的超时时间是否比先前的都早。如果是最早触发的,就会调用
ResetTimerfd
方法重新设置timerfd_的触发时间。内部会根据超时时间和现在时间计算出新的超时时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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);
if (eraliestChanged) { ResetTimerfd(timerfd_, timer->Expiration()); } }
|
内部实现的插入方法获取此定时器的超时时间,如果比先前的时间小就说明第一个触发。那么我们会设置好布尔变量。因此这涉及到timerfd_的触发时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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_
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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()"; } }
|
2.5 处理到期定时器
- EventLoop获取活跃的activeChannel,并分别调取它们绑定的回调函数。这里对于timerfd_,就是调用了HandleRead方法。
- HandleRead方法获取已经超时的定时器组数组,遍历到期的定时器并调用内部绑定的回调函数。之后调用
Reset
方法重新设置定时器
Reset
方法内部判断这些定时器是否是可重复使用的,如果是则继续插入定时器管理队列,之后自然会触发事件。如果不是,那么就删除。如果重新插入了定时器,记得还需重置timerfd_。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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); }
|
1 2 3 4 5 6 7 8 9 10 11
| std::vector<TimerQueue::Entry> TimerQueue::GetExpired(Timestamp now) { std::vector<Entry> expired; 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; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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; }
if (!timers_.empty()) { ResetTimerfd(timerfd_, (timers_.begin()->second)->Expiration()); } } }
|
3 muduo中定时器模块的特点
- 整个TimerQueue只使用一个timerfd来观察定时事件,并且每次重置timerfd时只需跟set中第一个节点比较即可,因为set本身是一个有序队列。
- 整个定时器队列采用了muduo典型的事件分发机制,可以使的定时器的到期时间像fd一样在Loop线程中处理。
- 之前Timestamp用于比较大小的重载方法在这里得到了很好的应用。