muduo之TimerQueue解析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014303647/article/details/88856399

首先直到Linux下获取当前时间的函数有:

time(2)/time_t(秒)

ftime(3)/ struct timeb(毫秒)

gettimeofday(2) /struct timeval(微秒)

clock_getime(2) / struct timespec (纳秒)

定时函数

sleep(3)

alarm(2)

usleep(3)

nanosleep(2)

clock_nanosleep(2)

getitimer(2) / setitimer(2)

timer_create(2) / timer_settime(2) / timer_gettime(2) / timer_delete(2)

timerfd_create(2) / timerfd_gettime(2) / timerfd_settime(2)

muduo是采用
计时:只使用gettimeofday(2)获取当前时间
定时: 只使用timerfd_*系列函数来处理定时任务

gettimeofday入选的原因是

1:time的精读太低,ftime已被废弃,clock_time精度最高,但是系统调用开销比gettimeofday大
2:在x86-64平台上,gettimeofday不是系统调用,而是在用户态完成的,没有上下文切换和陷入内核的开销
3: gettimeofday的分辨率在微秒级,足以满足日常计时的需要

timerfd_*入选的原因

1:sleep / alarm / usleep在实现时有可能使用了SIGALRM信号,多线程程序中尽量避免使用信号,因为处理起来比较麻烦(信号通知进程,所有线程都将接收到这个信号,谁处理好)。另外,如果网络库定义了信号处理函数,用户代码(main函数等使用库的程序)也定义了信号处理函数,这不就冲突了,该调用哪个好

2:nanosleep/clock_nanosleep是线程安全的,但是会让当前线程挂起一段时间,这会导致线程失去响应。

3:getitimer/ setitimer也是用信号来传递超时消息

3:timerfd_create把时间变成一个文件描述符,该“”文件“”在定时器超时的那一刻变为可读,这样就能很方便地融入select(2)/poll(2)框架中,用统一的方式来处理IO事件和超时事件,这正是Reactor模式的长处。

4:传统的reactor利用select(2)/poll(2)/epoll(4)的timeout来实现定时功能的,但是poll(2)和epoll_wait的定时精度只有毫秒,远低于timerfd_settime(2)的精度。

gettimeofday

#include <sys/time.h>
int gettimeofday(struct timeval* tv, struct timezone* tz);
/* 
 * 返回后将目前的时间存放在tv中,把时区信息存放在tz中 
 * tz和tz都可以为NULL
 * 执行成功返回0,失败返回-1
 */

struct timeval{
    long tv_sec;   /* 秒 */
    long tv_usec;  /* 微秒 */
};

struct timeval timer;
gettimeofday(&timer, NULL);

在TimerQueue里有对timerfd_*的使用

首先是创建一个timerfd,使用函数timerfd_create

int createTimerfd()
{
  // CLOCK_MONOTONIC 以固定的速率运行,从不进行调整和复位,它不受任何系统time-of-day时钟
  //的影响
  int timerfd = ::timerfd_create(CLOCK_MONOTONIC,
                                 TFD_NONBLOCK | TFD_CLOEXEC);
  if (timerfd < 0)
  {
    LOG_SYSFATAL << "Failed in timerfd_create";
  }
  return timerfd;
}

首先我们先不说TimerQueue的实现,而是先说定时器是怎么实现的,在muduo里是有Timer这个类,就是一个定时器类,

先我们看定时器类的成员和成员函数。

private:
  const TimerCallback callback_;
  Timestamp expiration_;
  const double interval_;
  const bool repeat_;
  const int64_t sequence_;
  static AtomicInt64 s_numCreated_;

public:
  Timer(TimerCallback cb, Timestamp when, double interval)
    : callback_(std::move(cb)),
      expiration_(when),
      interval_(interval),
      repeat_(interval > 0.0),
      sequence_(s_numCreated_.incrementAndGet())
  { }

  void run() const
  {
    callback_();
  }

  Timestamp expiration() const  { return expiration_; }
  bool repeat() const { return repeat_; }
  int64_t sequence() const { return sequence_; }

  void restart(Timestamp now);

  static int64_t numCreated() { return s_numCreated_.get(); }

看看各成员代表什么?

 const TimerCallback callback_; / 回调函数是用户提供的
 Timestamp expiration_; 定时器的到期时间
 const double interval_; 定时器触发的时间间隔
 const bool repeat_; 定时器是否是重复触发的,即是不是周期性的
 const int64_t sequence_; 	定时器都有个序号
 static AtomicInt64 s_numCreated_; 已经创建了多少个定时器

然后看看重要成员函数

 Timer(TimerCallback cb, Timestamp when, double interval)
    : callback_(std::move(cb)),
      expiration_(when),
      interval_(interval),
      repeat_(interval > 0.0),
      sequence_(s_numCreated_.incrementAndGet())
  { }

最重要的当然是构造函数,这里对一个定时器类进行了初始化

记住,序号是原子性操作

s_numCreated_.incrementAndGet()

然后就是几个获取信息的get函数

 Timestamp expiration() const  { return expiration_; } // 获取到期时间
  bool repeat() const { return repeat_; } // 是否是重复的定时器
  int64_t sequence() const { return sequence_; } //定时器的序号

最重要的函数是重启函数,void restart(Timestamp now);这个函数是干嘛的呢?它是当定时器到期后,若这个定时器是周期性的定时器,那么需要重设,我们看看具体实现:

void Timer::restart(Timestamp now)
{
  if (repeat_)
  {
    expiration_ = addTime(now, interval_);
  }
  else
  {
    expiration_ = Timestamp::invalid();
  }
}

然后我们看另外一个类,就是Timerld,其实就是抽象出一个对象,只有定时器和序列号对象。

代码:


#ifndef MUDUO_NET_TIMERID_H
#define MUDUO_NET_TIMERID_H

#include <muduo/base/copyable.h>

namespace muduo
{
namespace net
{

class Timer;

///
/// An opaque identifier, for canceling Timer.
///
// TimerId类用于保存超时任务Timer和它独一无二的id
class TimerId : public muduo::copyable 
{
 public:
  TimerId()
    : timer_(NULL),
      sequence_(0)
  {
  }

  TimerId(Timer* timer, int64_t seq)
    : timer_(timer),
      sequence_(seq)
  {
  }

  // default copy-ctor, dtor and assignment are okay

  friend class TimerQueue;

 private:
  Timer* timer_;
  int64_t sequence_;
};

}  // namespace net
}  // namespace muduo

#endif  // MUDUO_NET_TIMERID_H

那么我们的主角TimerQueue就闪亮登场了,首先我们得知道TimerQueue是怎么管理定时器的,在muduo中使用的是set,有的人会问,set不允许重复的,那么怎么保证存储同一时刻的定时器,这时候我们就看看TimerQueue的成员变量

typedef std::pair<Timestamp, Timer*> Entry;
typedef std::set<Entry> TimerList;
TimerList timers_; 

这里我们是以一个pair为key值,就算有一样的时间,但是时间+定时器的组合肯定是不会冲突的。

然后muduo还维护了一个定时器列表,不过是以(定时器,序列号)为key的,
定义:

typedef std::pair<Timer*, int64_t> ActiveTimer;
typedef std::set<ActiveTimer> ActiveTimerSet;
ActiveTimerSet activeTimers_;

我们得知道任何时刻,ActiveTimerSet.size() == timers_.size(),

好接下来,就是增加定时器了,那么增加定时器是从哪里增加的呢?我们得知道它从哪里来到哪里,我们看看Eventloop里的几个函数来实现定时器的添加。

TimerId EventLoop::runAt(Timestamp time, TimerCallback cb)
{
    return timerQueue_->addTimer(std::move(cb), time, 0.0);
}

TimerId EventLoop::runAfter(double delay, TimerCallback cb)
{
    Timestamp time(addTime(Timestamp::now(), delay));
    return runAt(time, std::move(cb));
}

TimerId EventLoop::runEvery(double interval, TimerCallback cb)
{
  Timestamp time(addTime(Timestamp::now(), interval));
  return timerQueue_->addTimer(std::move(cb), time, interval);
}

可以看到它们都是调用的timerQueue::addTimer函数,所以我们来一探究竟

TimerId 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));
    return TimerId(timer, timer->sequence());
}

可以看到我们调用的是Eventloop::runInLoop函数,是可以跨线程调用的,这回当做一个任务,放到doPendingfunctors里面执行,这样我们就会执行TimerQueue::addTimerInLoop。我们看看这个函数实现:

void TimerQueue::addTimerInLoop(Timer* timer)
{
  loop_->assertInLoopThread();
  /* 返回true,说明timer被添加到set的顶部,作为新的根节点,需要更新timerfd的激活时间 */
  bool earliestChanged = insert(timer);


  // 只有在计时器为空的时候或者新加入的计时器的最早触发时间小于当前计时器的堆顶的最小值
  // 才需要用最近时间去更新
  if (earliestChanged)
  {
    resetTimerfd(timerfd_, timer->expiration());
  }
}

这里执行的是把定时器放到timers_和activeTimers_里,我们姑且先来看看这个函数的实现,在回来解释

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;
  }
  {
    //插入定时器
    std::pair<TimerList::iterator, bool> result
      = timers_.insert(Entry(when, timer));
    assert(result.second); (void)result;
  }
  {
    std::pair<ActiveTimerSet::iterator, bool> result
      = activeTimers_.insert(ActiveTimer(timer, timer->sequence()));
    assert(result.second); (void)result;
  }
  assert(timers_.size() == activeTimers_.size());
  return earliestChanged;
}
TimerList::iterator it = timers_.begin();
  if (it == timers_.end() || when < it->first)
  {
    earliestChanged = true;
  }

首先看这段代码,那个if是啥意思呢

就是如何此时定时器为空,或者这个定时器比原有定时器列表里最先触发的定时器的到期事件都早,那么我们需要 重设timerfd_的触发时间
earliestChanged 就是这个标志。

然后后面就是对timers_和activeTimers_进行填充。

然后我们回到TimerQueue::addTimerInLoop,如果我们需要更改timerfd_的到期时间,那么就得调用resetTimerfd(timerfd_, timer->expiration())

我们看这个函数,其实就是对这个基于时间的定时器的到期时间进行改变。

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

 /*  struct itimerspec {
               struct timespec it_interval;  /* Interval for periodic timer */
              /* struct timespec it_value;     /* Initial expiration */
  /*};*/
  int ret = ::timerfd_settime(timerfd, 0, &newValue, &oldValue);
  if (ret)
  {
    LOG_SYSERR << "timerfd_settime()";
  }
}

那么定期是加进来了,那么定时器如何触发呢?这个时候,最重要的一个文件描述符终于上台了,那就是

 const int timerfd_; 
 Channel timerfdChannel_;

就是这个,它是什么,它是管着触发的,它肯定每次是触发的开始,所以在TimerQueue初始化的时候,会对其进行设置。

 timerfdChannel_.setReadCallback(std::bind(&TimerQueue::handleRead, this));
  timerfdChannel_.enableReading();

这个timerfd_被一个timerfdChannel_包裹着,知道这个定时器文件描述符的到期时间到了,那么就会触发handleRead函数。

具体实现如下:

void TimerQueue::handleRead()
{
  loop_->assertInLoopThread();
  Timestamp now(Timestamp::now());
  //要读走这个事件,不然会一直触发
  readTimerfd(timerfd_, now);

  std::vector<Entry> expired = getExpired(now); //得到同时过期的定时器

  callingExpiredTimers_ = true;
  cancelingTimers_.clear();
  // safe to callback outside critical section
  for (const Entry& it : expired)
  {
    it.second->run();
  }
  callingExpiredTimers_ = false;

  //周期性的任务添加到set中,不过记得要重新计算超时时间
  reset(expired, now);
}

切记一定触发的瞬间就要吧timerfd_读走,否则还是会触发的,因为默认是采用的lt模式,然后就是找到过期的定时器保存到那个expired里注意,这个vector保存的肯定是同时到期的定时器,我们先走完流程,在具体讲解里面用到的函数。然后得到过期的定时器函数,然后就执行定时器的回调函数,然后我们需要对那些过期的定时器进行设置,为什么呢?因为里面可能保存着那种周期性触发的定时器

那么我们来看具体的函数TimerQueue::getExpired

std::vector<TimerQueue::Entry> TimerQueue::getExpired(Timestamp now)
{
  assert(timers_.size() == activeTimers_.size());
  std::vector<Entry> expired;
  Entry sentry(now, reinterpret_cast<Timer*>(UINTPTR_MAX));
  TimerList::iterator end = timers_.lower_bound(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);
    assert(n == 1); (void)n;
  }

  assert(timers_.size() == activeTimers_.size());
  return expired;
}

首先我们这里为什么要构造一个Entry sentry(now, reinterpret_cast<Timer*>(UINTPTR_MAX));这个我觉得应该是同一个时间到期里最大的一个定时器,所以可以把所有超时的定时器找出来,然后插入到过期定时器expired里面,并且将过期定时器从timers_里删除,并且相应的删除activeTimers_的定时器。当然最后timers_.size() == activeTimers_.size()

接下来就是对过期的定时器中的那些重复触发的定时器进行重设,那么就得看reset函数。

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())
    {
      // 重新调用addTimer
      it.second->restart(now);
      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);
  }
}

我们通过对定时器的repeat字段进行判断,来判断是不是需要进行重新设置。这个时候,因为我们最重要的纽带timerfd_已经触发了,所以它的时间是超时的,所以如果定时器列表里存在定时器,那么我们就得更新下一次到期时间,然后对timerfd_进行重新设置。

最后就是怎么去取消定时器。我们来看TimerQueue::cancel

void TimerQueue::cancel(TimerId timerId)
{
  loop_->runInLoop(
      std::bind(&TimerQueue::cancelInLoop, this, timerId));
}

因为有可能跨线程调用,所以使用Eventloop::runInLoop()函数。
我们来看TimerQueue::cancelInLoop具体的实现。

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())
  {
    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_)
  {
    cancelingTimers_.insert(timer);
  }
  assert(timers_.size() == activeTimers_.size());
}
cancelingTimers_保存被取消的定时器。

哇,终于写完了,大家可以自己用muduo自带的例子去测一下。

文章参考:<<Linux多线程服务端编程>>

猜你喜欢

转载自blog.csdn.net/u014303647/article/details/88856399