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中的回调函数。
image

//构造函数
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());
}
posted @ 2021-05-19 16:08  零十  阅读(290)  评论(3编辑  收藏  举报