muduo源码分析之TimerQueue定时器
相关文件
事件戳类,包含一个int64的时间戳数据成员
muduo/base/Timestamp.cc
muduo/base/Timestamp.h
内部定时器类,主要包含时间戳和回调函数
muduo/net/Timer.h
muduo/net/Timer.cc
TimerQueue类,管理定时器
muduo/net/TimerQueue.h
muduo/net/TimerQueue.cc
Timer标识类,包含Timer指针和下标,TimerQueue类用于取消定时器
muduo/net/TimerId.h
作用
由上可知muduo中的定时器涉及的类比较多,虽然Timer和TimerId都是简单的封装,但直接从TimerQueue说起又感觉跨度太大。虽然TimerQueue的对外接口只有addTimer()和cancel(),但EventLoop把它封装成了更好用的runAt()、runAfter()、runEvery()等函数。
那runAt()、runAfter()、runEvery()又是实现什么功能呢?
EventLoop.cc
//在某个时刻运行回调函数cb()
TimerId EventLoop::runAt(Timestamp time, TimerCallback cb)
{
return timerQueue_->addTimer(std::move(cb), time, 0.0);
}
//过一段时间(delay秒)运行回调函数cb()
TimerId EventLoop::runAfter(double delay, TimerCallback cb)
{
Timestamp time(addTime(Timestamp::now(), delay));
return runAt(time, std::move(cb));
}
//每隔interval秒运行一次回调函数cb()
TimerId EventLoop::runEvery(double interval, TimerCallback cb)
{
Timestamp time(addTime(Timestamp::now(), interval));
return timerQueue_->addTimer(std::move(cb), time, interval);
}
定时函数
所以定时器功能就是定时执行回调函数,定时函数这么多(sleep\alarm\setitimer\timerfd_create等),再看muduo中的定时器封装的是哪个定时函数。
TimerQueue.cc
int createTimerfd()
{
int timerfd = ::timerfd_create(CLOCK_MONOTONIC,
TFD_NONBLOCK | TFD_CLOEXEC);
if (timerfd < 0)
{
LOG_SYSFATAL << "Failed in timerfd_create";
}
return timerfd;
}
void resetTimerfd(int timerfd, Timestamp expiration)
{
// wake up loop by timerfd_settime()
struct itimerspec newValue;
struct itimerspec oldValue;
memZero(&newValue, sizeof newValue);
memZero(&oldValue, sizeof oldValue);
newValue.it_value = howMuchTimeFromNow(expiration);
int ret = ::timerfd_settime(timerfd, 0, &newValue, &oldValue);
if (ret)
{
LOG_SYSERR << "timerfd_settime()";
}
}
timerfd_create把时间变成了一个文件描述符,该“文件”在定时器超时的那一刻变得可读,这样就能很方便地融入到select/poll框架中,用统一的方式处理IO事件和超时事件。
再看一个直接使用timerfd实现的单次触发定时器示例。
timerfd_create()创建文件描述符,timerfd_settime()设置定时时间。
如果要实现循环每隔一段时间执行的话,在timeout函数中重新设置定时时间即可。
#include <muduo/net/Channel.h>
#include <muduo/net/EventLoop.h>
#include <boost/bind.hpp>
#include <stdio.h>
#include <sys/timerfd.h>
using namespace muduo;
using namespace muduo::net;
EventLoop* g_loop;
int timerfd;
void timeout(Timestamp receiveTime)
{
printf("Timeout!\n");
uint64_t howmany;
::read(timerfd, &howmany, sizeof howmany);//不读出的话会一直触发可读事件
g_loop->quit();//退出
}
int main(void)
{
EventLoop loop;
g_loop = &loop;
timerfd = ::timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
Channel channel(&loop, timerfd);
channel.setReadCallback(boost::bind(timeout, _1));//读事件回调函数
channel.enableReading();//设置监听该通道的可读事件
struct itimerspec howlong;
bzero(&howlong, sizeof howlong);//清零初始化,memset 0
howlong.it_value.tv_sec = 1; //设置1秒后触发
::timerfd_settime(timerfd, 0, &howlong, NULL);
loop.loop();
::close(timerfd);
}
使用
本着先知道怎么用,再知道怎么实现的循序渐进的原则,还是再看一下muduo中定时器的使用。
前面说到muduo在EventLoop中将定时器封装成更方便使用的函数,下面是一个使用示例:
#include <muduo/net/EventLoop.h>
#include <muduo/base/Thread.h>
#include <boost/bind.hpp>
#include <stdio.h>
#include <unistd.h>
using namespace muduo;
using namespace muduo::net;
int cnt = 0;
EventLoop* g_loop;
void printTid()
{
printf("pid = %d, tid = %d\n", getpid(), CurrentThread::tid());//线程tid
printf("now %s\n", Timestamp::now().toString().c_str()); //时间戳
}
void print(const char* msg)
{
printf("msg %s %s\n", Timestamp::now().toString().c_str(), msg);
if (++cnt == 20)
{
g_loop->quit();//打印20条信息后退出
}
}
void cancel(TimerId timer)
{
g_loop->cancel(timer); //取消定时器
printf("cancelled at %s\n", Timestamp::now().toString().c_str());
}
int main()
{
printTid();
sleep(1);
{
EventLoop loop;
g_loop = &loop;
print("main");
loop.runAfter(1, boost::bind(print, "once1")); //1秒后运行print
loop.runAfter(1.5, boost::bind(print, "once1.5"));
loop.runAfter(2.5, boost::bind(print, "once2.5"));
loop.runAfter(3.5, boost::bind(print, "once3.5"));
TimerId t45 = loop.runAfter(4.5, boost::bind(print, "once4.5"));
loop.runAfter(4.2, boost::bind(cancel, t45)); //这里在4.2秒取消了定时器t45,上面的4.5秒print不会执行
loop.runAfter(4.8, boost::bind(cancel, t45)); //重复取消,没关系
loop.runEvery(2, boost::bind(print, "every2"));
TimerId t3 = loop.runEvery(3, boost::bind(print, "every3"));
loop.runAfter(9.001, boost::bind(cancel, t3));
loop.loop();
print("main loop exits");
}
}
TimerQueue源码分析
有了上面的预备认识,就可以深入TimerQueue源码看其实现了。
从上面知道TimerQueue的对外接口就两个,addTimer()和cancel(),一个用来添加定时器,一个用来取消定时器。
TimerQueue的作用就是管理多个定时器,当定时器到期,执行它的回调函数。
定时器Timer类就只封装了到期时间戳和回调函数指针,timerfd_settime()定时函数的执行在TimerQueue中,也就是,把所有定时器放进一个一时间戳从小到大排序的容器中,每次timerfd_settime()的定时时间即容器的第一个定时器的时间戳,因为下一个就轮到它触发了。
这个容器,在muduo中选择的是set,key是时间戳和定时器timer指针组成的pair。能快速根据当前时间找到已到期的定时器,也能高效的添加和删除Timer。
TimerQueue类
/// A best efforts timer queue.
/// No guarantee that the callback will be on time.
///
class TimerQueue : noncopyable
{
public:
explicit 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(TimerCallback cb,
Timestamp when,
double interval); //添加定时器
void cancel(TimerId timerId); //取消定时器
private:
// FIXME: use unique_ptr<Timer> instead of raw pointers.
// This requires heterogeneous comparison lookup (N3465) from C++14
// so that we can find an T* in a set<unique_ptr<T>>.
typedef std::pair<Timestamp, Timer*> Entry; //时间戳,Timer指针
typedef std::set<Entry> TimerList;
typedef std::pair<Timer*, int64_t> ActiveTimer; //TimerId的两个主要的数据成员
typedef std::set<ActiveTimer> ActiveTimerSet;
void addTimerInLoop(Timer* timer);
void cancelInLoop(TimerId timerId);
// called when timerfd alarms
void handleRead();
// move out all expired timers
//返回超时的定时器列表
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_;
Channel timerfdChannel_;
// Timer list sorted by expiration
TimerList timers_; //timers_是按到期时间排序
// for cancel()
ActiveTimerSet activeTimers_;
bool callingExpiredTimers_; /* atomic */
ActiveTimerSet cancelingTimers_; //保存的是被取消的定时器
};
通道注册与回调函数
在TimerQueue的构造函数中,想EventLopp注册timerfd_create()产生的文件描述符的通道,这样就Poller就能监听到该“文件”的读事件。
当读事件到来,回调TimerQueue的handleRead函数,这个函数调用getExpired()将set容器中的到期定时器Timer取出,再执行Timer中的回调函数。
//构造函数
TimerQueue::TimerQueue(EventLoop* loop)
: loop_(loop),
timerfd_(createTimerfd()), //调用timerfd_create()得到fd
timerfdChannel_(loop, timerfd_),//通道初始化
timers_(),
callingExpiredTimers_(false)
{
timerfdChannel_.setReadCallback(
std::bind(&TimerQueue::handleRead, this));//注册读事件回调函数
// we are always reading the timerfd, we disarm it with timerfd_settime.
timerfdChannel_.enableReading();//更新后通道加入到loop中
}
//timefd 读事件回调函数
void TimerQueue::handleRead()
{
loop_->assertInLoopThread();
Timestamp now(Timestamp::now()); //当前时间戳
readTimerfd(timerfd_, now); //封装read(),清除该事件,避免一直触发
//获取该时刻之前所有的定时器列表
std::vector<Entry> expired = getExpired(now);
callingExpiredTimers_ = true;
cancelingTimers_.clear();
// safe to callback outside critical section
for (const Entry& it : expired)
{
it.second->run(); //执行每个到期Timer的回调函数
}
callingExpiredTimers_ = false;
reset(expired, now); //循环执行的定时器需要重置
}
取出到期定时器
因为set是有序的,使用lower_bound搜索。
使用copy复制出到期的定时器。
std::vector<TimerQueue::Entry> TimerQueue::getExpired(Timestamp now)
{
assert(timers_.size() == activeTimers_.size());//timers_即装timer的set容器
std::vector<Entry> expired; //头文件中typedef std::pair<Timestamp, Timer*> Entry;
Entry sentry(now, reinterpret_cast<Timer*>(UINTPTR_MAX));//当前时间戳+最大的指针地址
TimerList::iterator end = timers_.lower_bound(sentry); //返回第一个大于等于sentry的迭代器
assert(end == timers_.end() || now < end->first);
std::copy(timers_.begin(), end, back_inserter(expired));//时间戳小于当前时间的放进数组
timers_.erase(timers_.begin(), end);//移除
for (const Entry& it : expired)
{
ActiveTimer timer(it.second, it.second->sequence());
size_t n = activeTimers_.erase(timer); //在activeTimers_中移除
assert(n == 1); (void)n;
}
assert(timers_.size() == activeTimers_.size());
return expired;
}
重置定时器
输入为getExpired()返回的数组
如果里面的定时器是设置了是重复执行的,则调用Timer的restart(),修改Timer到期时间,重新插入TimerQueue。
最后获取下一个到期时间,设置timefd的下次触发。
void TimerQueue::reset(const std::vector<Entry>& expired, Timestamp now)
{
Timestamp nextExpire;
for (const Entry& it : expired) //遍历数组
{
ActiveTimer timer(it.second, it.second->sequence());
//如果是重复的定时器并且是未取消定时器则重启该定时器
if (it.second->repeat()
&& cancelingTimers_.find(timer) == cancelingTimers_.end())
{
it.second->restart(now);//Timer中的方法
insert(it.second);
}
else
{
// FIXME move to a free list
//一次性定时器是不能重置的,删除它
delete it.second; // FIXME: no delete please
}
}
if (!timers_.empty())
{
//获取最早到期的定时器超时时间
nextExpire = timers_.begin()->second->expiration();
}
if (nextExpire.valid()) //下一个到期时间大于零
{
resetTimerfd(timerfd_, nextExpire); //设置
}
}
添加定时器
addTimer()->addTimerInLoop()->insert()。
可以暂时不管runInLoop是什么,其作用是将任务分发到IO线程执行。
TimerId TimerQueue::addTimer(TimerCallback cb,
Timestamp when,
double interval)
{
//cb回调函数,when时间戳,interval每隔多少秒调用一次
Timer* timer = new Timer(std::move(cb), when, interval); //new一个timer对象
loop_->runInLoop(
std::bind(&TimerQueue::addTimerInLoop, this, timer));
return TimerId(timer, timer->sequence());
}
void TimerQueue::addTimerInLoop(Timer* timer)
{
loop_->assertInLoopThread();
bool earliestChanged = insert(timer);
if (earliestChanged) //插入的Timer的到期时间比原来都早
{
//重置timerfd的下次触发时间
resetTimerfd(timerfd_, timer->expiration());
}
}
bool TimerQueue::insert(Timer* timer)
{
loop_->assertInLoopThread();
assert(timers_.size() == activeTimers_.size());
bool earliestChanged = false;
Timestamp when = timer->expiration();
TimerList::iterator it = timers_.begin();
if (it == timers_.end() || when < it->first)
{
earliestChanged = true; //如果插入的Timer的到期时间比原来都早,则需要重置timefd的下次触发时间。
}
{
std::pair<TimerList::iterator, bool> result
= timers_.insert(Entry(when, timer)); //插入的timers中
assert(result.second); (void)result;
}
{
std::pair<ActiveTimerSet::iterator, bool> result
= activeTimers_.insert(ActiveTimer(timer, timer->sequence()));//sequence相当于Timer的一个创建的序号,activeTimers_的数量和timers_始终保持一致
assert(result.second); (void)result;
}
assert(timers_.size() == activeTimers_.size());
return earliestChanged;
}
取消定时器
cancel()->cancelInLoop()
先在activeTimers_中找到要取消的定时器,再根据该定时器的时间戳和指针在timers_中删除,再在activeTimers_中删除。
为什么是这样的曲线救国呢?
TimerId是用户能看到的类,Timer和TimerQueue是内部类。
用户调用runAt()等接口返回TimerId,再用这个TimerId取消定时器。
所以cancle的输入是TimerId,activeTimers_这个set里的数据也是各个定时器的TimerId(指针加序号)
timers_这个set里的数据则为了排序是(时间戳+指针),这个时间戳如果是循环定时器的话,再次插入时是变化的,不能作为定时器的标识。
所以使用TimerId(指针加序号)作为定时器的标识。
所以activeTimers_和timers_中定时器的数量示保持一致的,插入时一起插入,删除时一起删除。
activeTimers_就是为了帮助取消定时器而存在的。
- 在activeTimers_中,直接删掉;
- 不在activeTimers_中并且当前handLeRead在执行定时器的cb函数,则添加到cancelingTimers_;在调用reset()时,cancelingTimers_中的定时器不会被重置,当再次触发handleRead,cancelingTimers_会清空。
void TimerQueue::cancel(TimerId timerId)
{
loop_->runInLoop(
std::bind(&TimerQueue::cancelInLoop, this, timerId));
}
void TimerQueue::cancelInLoop(TimerId timerId)
{
loop_->assertInLoopThread();
assert(timers_.size() == activeTimers_.size());
ActiveTimer timer(timerId.timer_, timerId.sequence_);
ActiveTimerSet::iterator it = activeTimers_.find(timer);
if (it != activeTimers_.end())//还在set中,直接删掉
{
size_t n = timers_.erase(Entry(it->first->expiration(), it->first));
assert(n == 1); (void)n;
delete it->first; // FIXME: no delete please
activeTimers_.erase(it);
}
else if (callingExpiredTimers_)//不在set中并且现在在调用定时器函数
{
cancelingTimers_.insert(timer);
//添加到取消的set中,这个set在handread中会清空
//添加到这个set中的为了告诉reset函数,在这里面的定时器就别重置了
}
assert(timers_.size() == activeTimers_.size());
}