WebServer(6) 定时器模块

本文最后更新于:8 个月前

WebServer(6) 定时器模块

写在前面

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

正文

1 Timer类

一个定时器需要哪些属性呢?

  1. 定时器到期后需要调用回调函数
  2. 我们需要让定时器记录我们设置的超时时间
  3. 如果是重复事件(比如每间隔5秒扫描一次用户连接),我们还需要记录超时时间间隔
  4. 对应的,我们需要一个 bool 类型的值标注这个定时器是一次性的还是重复的
1
2
3
4
5
6
7
class Timer : noncopyable {{
private:
const TimerCallback callback_; // 定时器回调函数
Timestamp expiration_; // 下一次的超时时刻
const double interval_; // 超时时间间隔,如果是一次性定时器,该值为0
const bool repeat_; // 是否重复(false 表示是一次性定时器)
};

其他的方法比较简单,基本是一些定时器属性的返回。

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) { // 一次性定时器设置为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

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>;

// 在本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_; // 正在获取超时定时器标志
};

2.1 使用timerfd实现定时功能

linux2.6.25 版本新增了timerfd这个供用户程序使用的定时接口,这个接口基于文件描述符,当超时事件发生时,该文件描述符就变为可读。这种特性可以使我们在写服务器程序时,很方便的便把定时事件变成和其他I/O事件一样的处理方式,当时间到期后,就会触发读事件。我们调用响应的回调函数即可。

2.1.1 timer_create
1
int timer_create(int clockid,int flags); // 成功返回0

此函数用于创建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);
//成功返回0

我们使用此函数启动或停止定时器。

  • fd:timerfd_create()返回的文件描述符
  • flags:0表示相对定时器,TFD_TIMER_ABSTIME表示绝对定时器
  • new_value:设置超时时间,设置为0则表示停止定时器
  • old_value:原来的超时时间,不使用可以置为NULL

2.2 创建TimerQueue

  1. 通过timer_create创建timerfd
  2. TimerQueue类初始化后,设置其timerfdChannel绑定读事件,并置于可读状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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();
}

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 插入定时器流程

  1. EventLoop调用方法,加入一个定时器事件,会向里传入定时器回调函数,超时时间和间隔时间(为0.0则为一次性定时器),AddTimer方法根据这些属性构造新的定时器。
  2. 定时器队列内部插入此定时器,并判断这个定时器的超时时间是否比先前的都早。如果是最早触发的,就会调用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);

// 我们需要重新设置timerfd_触发时间
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
// 重置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()";
}
}

2.5 处理到期定时器

  1. EventLoop获取活跃的activeChannel,并分别调取它们绑定的回调函数。这里对于timerfd_,就是调用了HandleRead方法。
  2. HandleRead方法获取已经超时的定时器组数组,遍历到期的定时器并调用内部绑定的回调函数。之后调用Reset方法重新设置定时器
  3. 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<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;
}
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;
}

// 如果重新插入了定时器,需要继续重置timerfd
if (!timers_.empty()) {
ResetTimerfd(timerfd_, (timers_.begin()->second)->Expiration());
}
}
}

3 muduo中定时器模块的特点

  1. 整个TimerQueue只使用一个timerfd来观察定时事件,并且每次重置timerfd时只需跟set中第一个节点比较即可,因为set本身是一个有序队列。
  2. 整个定时器队列采用了muduo典型的事件分发机制,可以使的定时器的到期时间像fd一样在Loop线程中处理。
  3. 之前Timestamp用于比较大小的重载方法在这里得到了很好的应用。