定时器功能相关的类由 Timestamp,Timer,TimerQueue类组成,muduo 库还有一个 Timeld 类方便对定时器进行索引,本项目里没有加上这个类。
一个定时器需要哪些属性呢?
- 定时器到期后需要调用回调函数
- 我们需要让定时器记录我们设置的超时时间
- 如果是重复事件(比如每间隔5秒扫描一次用户连接),我们还需要记录超时时间间隔
- 对应的,我们需要一个 bool 类型的值标注这个定时器是一次性的还是重复的
class Timer : noncopyable
{
private:
const TimerCallback callback_; // 定时器回调函数
Timestamp expiration_; // 下一次的超时时刻
const double interval_; // 超时时间间隔,如果是一次性定时器,该值为0
const bool repeat_; // 是否重复(false 表示是一次性定时器)
};
其他的方法比较简单,基本是一些定时器属性的返回。
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
。
void Timer::restart(Timestamp now)
{
if (repeat_)
{
// 如果是重复定时事件,则继续添加定时事件,得到新事件到期事件
expiration_ = addTime(now, interval_);
}
else
{
expiration_ = Timestamp();
}
}
TimerQueue类管理作为管理定时器的结构。其内部使用 STL 容器 set 来管理定时器。我们以时间戳作为键值来获取定时器。set 内部实现是红黑树,红黑树中序遍历便可以得到按照键值排序过后的定时器节点。
using Entry = std::pair<Timestamp, Timer*>;
using TimerList = std::set<Entry>;
TimerQueue类方法和成员变量
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_; // 正在获取超时定时器标志
};
linux2.6.25 版本新增了timerfd这个供用户程序使用的定时接口,这个接口基于文件描述符,当超时事件发生时,该文件描述符就变为可读。这种特性可以使我们在写服务器程序时,很方便的便把定时事件变成和其他I/O事件一样的处理方式,当时间到期后,就会触发读事件。我们调用响应的回调函数即可。
int timer_create(int clockid,int flags); // 成功返回0
此函数用于创建timerfd,我们需要指明使用标准事件还是相对事件,并且传入控制标志。
- CLOCK_REALTIME:相对时间,从1970.1.1到目前时间,之所以说其为相对时间,是因为我们只要改变当前系统的时间,从1970.1.1到当前时间就会发生变化,所以说其为相对时间
- CLOCK_MONOTONIC:绝对时间,获取的时间为系统最近一次重启到现在的时间,更该系统时间对其没影响
- flag:TFD_NONBLOCK(非阻塞),TFD_CLOEXEC
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
- 通过timer_create创建timerfd
- TimerQueue类初始化后,设置其timerfdChannel绑定读事件,并置于可读状态
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();
}
TimerQueue::~TimerQueue()
{
timerfdChannel_.disableAll();
timerfdChannel_.remove();
::close(timerfd_);
// 删除所有定时器
for (const Entry& timer : timers_)
{
delete timer.second;
}
}
- EventLoop调用方法,加入一个定时器事件,会向里传入定时器回调函数,超时时间和间隔时间(为0.0则为一次性定时器),
addTimer
方法根据这些属性构造新的定时器。 - 定时器队列内部插入此定时器,并判断这个定时器的超时时间是否比先前的都早。如果是最早触发的,就会调用
resetTimerfd
方法重新设置tiemrfd_的触发时间。内部会根据超时时间和现在时间计算出新的超时时间。
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_的触发时间。
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_
// 重置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()");
}
}
- EventLoop获取活跃的activeChannel,并分别调取它们绑定的回调函数。这里对于timerfd_,就是调用了handleRead方法。
- handleRead方法获取已经超时的定时器组数组,遍历到期的定时器并调用内部绑定的回调函数。之后调用
reset
方法重新设置定时器 reset
方法内部判断这些定时器是否是可重复使用的,如果是则继续插入定时器管理队列,之后自然会触发事件。如果不是,那么就删除。如果重新插入了定时器,记得还需重置timerfd_。
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);
}
// 返回删除的定时器节点 (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;
}
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());
}
}
}
- 整个TimerQueue只使用一个timerfd来观察定时事件,并且每次重置timerfd时只需跟set中第一个节点比较即可,因为set本身是一个有序队列。
- 整个定时器队列采用了muduo典型的事件分发机制,可以使的定时器的到期时间像fd一样在Loop线程中处理。
- 之前Timestamp用于比较大小的重载方法在这里得到了很好的应用