muduo网络库学习(三)定时器TimerQueue的设计

Linux下用于获取当前时间的函数有

  • time(2) / time_t (秒)
  • ftime(3) / struct timeb (毫秒)
  • gettimeofday(2) / struct timeval (微秒)
  • clock_gettime(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_gettime精度最高,但是系统调用开销比gettimeofday大
  2. 在x86-64平台上,gettimeofday不是系统调用,而是在用户态完成的,没有上下文切换和陷入内核的开销
  3. gettimeofday的分辨率在微秒级,足以满足日常计时的需要

timerfd*_入选的原因

  1. sleep / alarm / usleep在实现时有可能使用了SIGALRM信号,多线程程序中尽量避免使用信号,因为处理起来比较麻烦(信号通知进程,所有线程都将接收到这个信号,谁处理好)。另外,如果网络库定义了信号处理函数,用户代码(main函数等使用库的程序)也定义了信号处理函数,这不就冲突了,该调用哪个好
  2. nanosleep / clock_nanosleep是线程安全的,但是会让当前线程挂起等待一段时间,这会导致线程失去响应
  3. gettimer和timer_create也是用信号来传递超时信息
  4. timerfd_create把时间变成了一个文件描述符,该描述符在定时器超时的那一刻变为可读,可以很方便的融入到select/poll/epoll中,统一事件源
  5. select/poll/epoll可以设置timeout来实现超时,但是poll/epoll的精度只有毫秒,远低于timerfd_settime

                        ---------------------------------------摘自《Linux多线程服务器编程》
    

gettimeofday

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

timerfd_*系列函数

#include 
int timerfd_create(int clockid, int flags);
/*
 * 创建一个用于定时器的文件描述符
 * clockid可以为
 *     CLOCK_REALTIME(系统实时时间,可能会被用户手动更改)
 *     CLOCK_MONOTONIC(从系统启动那一刻开始计时的时间,无法被用户更改)
 * flags可以为
 *     TFD_NONBLOCK(非阻塞)
 *     TFD_CLOEXEC(调用exec时自动close)
 */


int timerfd_settime(int fd, int flag, 
                    const struct itimerspec* new_value,
                    const struct itimerspec* old_value);
/*
 * 用于设置timerfd的超时时间
 * fd, 通过timerfd_create返回的文件描述符
 * flag, 0代表相对时间,TFD_TIMER_ABSTIME代表绝对时间
 * new_value, 新设置的超时时间
 * old_value, 以前设置的超时时间,值-结果参数
 *  
 * struct timespec{
 *     time_t tv_sec;
 *     time_t tv_nsec;
 * };
 * 
 * struct itimerspec{
 *     struct timespec it_interval;
 *     struct timespce it_value;
 * }; 
 *  
 * it_value表示首次超时时间
 * it_interval表示后续周期性超时时间
 * it_interval不为0表示是周期性超时任务
 * it_interval和it_value同时为0表示取消定时任务
 */


int timerfd_gettime(int fd, struct itimerspec *cur_value);
/*
 * 返回距离下次超时还剩多长时间,保存在cur_value中
 * 如果调用时定时器已经到期,同时定时器设置了周期性任务(it_interval不为0)
 * 那么调用此函数之后定时器重新开始计时,超时时间是it_interval的值
 */

Timestamp类用于保存超时时间,类中只有一个成员变量,保存当前UTC时间,即从Unix Epoch(1970-01-01 00:00:00)到现在的微秒数
Timer类是一个超时任务,保存超时时间Timestamp,回调函数,以及记录自己是否是周期性计时任务,回调函数是用户提供的
TimerId类用于保存超时任务Timer和它独一无二的id
TimerQueue类保存用户设置的所有超时任务,需要高效保存尚未超时的任务,同时需要有序,方便找到超时时间最近的那个任务,可以用最小堆(libevent采用),也可以用std::set存储(muduo采用)

二者不同之处在于
- libevent将超时任务也封装在struct event中(类似muduo的Channel),最小堆中存放的也就直接是struct event,这么做的原因是libevent支持对某个文件描述符的超时监听,包括tcp连接的fd。
- muduo是另封装了一个Timer用与表示定时任务,并采用std::pair作为std::set的键,通过比较超时时间来排序(std::set内部采用红黑树,一种二叉搜索树)


对于定时任务的原理,

  1. muduo采用timerfd_*将超时任务转换成文件描述符进行监听
  2. 当时间一到,timerfd变为可读,相应的Channel调用回调函数(TimerQueue::handleRead
  3. 回调函数中将所有在TimerQueue中的超时任务找出,一次调用其回调函数
  4. 对于周期性定时任务,再添加回TimerQueue

muduo网络库学习(三)定时器TimerQueue的设计_第1张图片

整个过程只有一个timerfdPoller监听,所以调用timerfd_settime设置的超时时间一定是TimerQueueset里最小的,即set.begin();第一个Timer任务。
而用户是通过调用EventLoop::runAt/runAfter/runEvery函数注册定时任务的,这些函数都需要向TimerQueueset中添加Timer,所以每添加一个都需要判断新添加的定时任务的超时时间是否小于设置的超时时间,如果小于,就需要调用timerfd_settime重新设置timerfd的超时时间。
而每次timerfd被激活都需要找到在set中所有的超时任务,因为有可能存在超时时间相等的定时任务,可以使用std::lower_bound函数找到第一个大于等于给定值的位置


TimerQueue由所在的EventLoop持有,用户设置定时任务也是调用的EventLoop的接口,进而调用TimerQueue的接口
EventLoop提供三个接口用于注册定时任务,进而调用TimerQueueaddTimer接口

/* 
 * 定时器功能,由用户调用runAt并提供当事件到了执行的回调函数
 * 时间在Timestamp设置,绝对时间,单位是微秒
 */
TimerId EventLoop::runAt(Timestamp time, TimerCallback cb)
{
  /* std::move,移动语义,避免拷贝 */
  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的定义如下,主要就是保存着timerfd和所有的定时任务,回调函数,以及添加/删除定时任务的函数

/* 前向声明,避免#include */
class EventLoop;
class Timer;
class TimerId;

///
/// 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.
  /* 
   * 用于注册定时任务
   * @param cb, 超时调用的回调函数
   * @param when,超时时间(绝对时间)
   * @interval,是否是周期性超时任务
   */
  TimerId addTimer(TimerCallback cb,
                   Timestamp when,
                   double interval);

  /* 取消定时任务,每个定时任务都有对应的TimerId,这是addTimer返回给调用者的 */
  void cancel(TimerId timerId);

 private:

  // FIXME: use unique_ptr instead of raw pointers.
  // This requires heterogeneous comparison lookup (N3465) from C++14
  // so that we can find an T* in a set>.
  typedef std::pair Entry;
  typedef std::set TimerList;
  /* 
   * 主要用于删除操作,通过TimerId找到Timer*,再通过Timer*找到在timers_中的位置,将期删除
   * 觉得可以省略
   */
  typedef std::pair ActiveTimer;
  typedef std::set ActiveTimerSet;

  void addTimerInLoop(Timer* timer);
  void cancelInLoop(TimerId timerId);
  // called when timerfd alarms
  /* 当timerfd被激活时调用的回调函数,表示超时 */
  void handleRead();
  // move out all expired timers
  /* 从timers_中拿出所有超时的Timer* */
  std::vector getExpired(Timestamp now);
  /* 将超时任务中周期性的任务重新添加到timers_中 */
  void reset(const std::vector& expired, Timestamp now);

  /* 插入到timers_中 */
  bool insert(Timer* timer);

  /* 所属的事件驱动循环 */
  EventLoop* loop_;
  /* 由timerfd_create创建的文件描述符 */
  const int timerfd_;
  /* 用于监听timerfd的Channel */
  Channel timerfdChannel_;
  // Timer list sorted by expiration
  /* 保存所有的定时任务 */
  TimerList timers_;

  // for cancel()
  ActiveTimerSet activeTimers_;
  bool callingExpiredTimers_; /* atomic */
  ActiveTimerSet cancelingTimers_;
};

.cpp中主要是添加和回调函数,添加函数是addTimer,由用户调用EventLoop::run*,再由runAt调用addTimer

/*
 * 用户调用runAt/runAfter/runEveny后由EventLoop调用的函数
 * 向时间set中添加时间
 * 
 * @param cb,用户提供的回调函数,当时间到了会执行
 * @param when,超时时间,绝对时间
 * @param interval,是否调用runEveny,即是否是永久的,激活一次后是否继续等待
 * 
 * std::move,避免拷贝,移动语义
 * std::bind,绑定函数和对象,生成函数指针
 */
TimerId TimerQueue::addTimer(TimerCallback cb,
                             Timestamp when,
                             double interval)
{
  Timer* timer = new Timer(std::move(cb), when, interval);
  /* 
   * 在自己所属线程调用addTimerInLoop函数 
   * 用户只能通过初始创建的EventLoop调用addTimer,为什么还会考虑线程问题 why?
   * 这个线程和TcpServer的线程应该是同一个
   */
  loop_->runInLoop(
      std::bind(&TimerQueue::addTimerInLoop, this, timer));
  return TimerId(timer, timer->sequence());
}

函数中主要将执行任务交给addTimerInLoop,感觉这里不太需要这样做

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

  if (earliestChanged)
  {
    resetTimerfd(timerfd_, timer->expiration());
  }
}

插入函数主要任务是将某个定时任务插入到定时任务set中,同时判断新添加的这个定时任务的超时时间和之前设置的超时时间的大小(位于set的根节点处),如果新添加的定时任务超时时间小,就需要更新timerfd的超时时间(返回true),然后调用resetTimerfd使用timerfd_settime重新设置超时时间

bool TimerQueue::insert(Timer* timer)
{
  loop_->assertInLoopThread();
  assert(timers_.size() == activeTimers_.size());
  bool earliestChanged = false;
  /* 获取timer的UTC时间戳,和timer组成std::pair */
  Timestamp when = timer->expiration();
  /* timers_begin()是set顶层元素(红黑树根节点),是超时时间最近的Timer* */
  TimerList::iterator it = timers_.begin();
  /* 如果要添加的timer的超时时间比timers_中的超时时间近,更改新的超时时间 */
  if (it == timers_.end() || when < it->first)
  {
    earliestChanged = true;
  }
  {
    /* 添加到定时任务的set中 */
    std::pairbool> result
      = timers_.insert(Entry(when, timer));
    assert(result.second); (void)result;
  }
  {
    /* 同时也添加到activeTimers_中,用于删除时查找操作 */
    std::pairbool> result
      = activeTimers_.insert(ActiveTimer(timer, timer->sequence()));
    assert(result.second); (void)result;
  }

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


timerfd被激活,表明定时任务超时,进而调用回调函数,即TimerQueue::handleRead,这个回调函数是在构造函数中构造Channel时候注册的

回调函数先调用getExpired从定时任务set中取出所有超时任务,然后执行其回调函数,最后判断取出的这些超时任务有没有周期性的,如果有,就将周期性任务添加回set

/* 
 * 当定时器超时,保存timerfd的Channel激活,调用回调函数
 */
void TimerQueue::handleRead()
{
  loop_->assertInLoopThread();
  Timestamp now(Timestamp::now());
  readTimerfd(timerfd_, now);

  /* 从定时任务set中拿出所有超时任务 */
  std::vector expired = getExpired(now);

  callingExpiredTimers_ = true;
  cancelingTimers_.clear();
  // safe to callback outside critical section
  /* 调用超时的事件回调函数 */
  for (std::vector::iterator it = expired.begin();
      it != expired.end(); ++it)
  {
    it->second->run();
  }
  callingExpiredTimers_ = false;

  reset(expired, now);
}

getExpired主要就是将set中的超时任务拿出,因为set是有序的,直接调用std::lower_bound找到第一个大于等于当前时间的定时任务,前面的所有任务都是超时的,全部取出
感觉应该使用std::upper_bound找到第一个大于当前时间的任务,如果超时时间和当前时间相等,应该算作超时才对?

/*
 * 重新整理时间set中的任务,将所有超时的任务都拿出,然后调用其回调函数
 */
std::vector TimerQueue::getExpired(Timestamp now)
{
  assert(timers_.size() == activeTimers_.size());
  std::vector expired;
  Entry sentry(now, reinterpret_cast(UINTPTR_MAX));
  /* lower_bound:找到第一个大于等于参数的位置,返回迭代器(此处如果超时时间恰好是now,应该不算作超时) */
  TimerList::iterator end = timers_.lower_bound(sentry);
  assert(end == timers_.end() || now < end->first);
  /* back_inserter:容器适配器,将数据插入到参数的尾部 */
  std::copy(timers_.begin(), end, back_inserter(expired));
  timers_.erase(timers_.begin(), end);

  for (std::vector::iterator it = expired.begin();
      it != expired.end(); ++it)
  {
    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;
}

调用完回调函数之后需要将周期性任务重新添加到set中,不过记得要重新计算超时时间

/* 
 * 调用完所有超时的回调函数后,需要对这些超时任务进行整理
 * 将周期性的定时任务重新添加到set中
 */
void TimerQueue::reset(const std::vector& expired, Timestamp now)
{
  Timestamp nextExpire;

  for (std::vector::const_iterator it = expired.begin();
      it != expired.end(); ++it)
  {
    ActiveTimer timer(it->second, it->second->sequence());
    if (it->second->repeat() /* 是否是周期性的定时任务 */
        && cancelingTimers_.find(timer) == cancelingTimers_.end()) /* 如果用户手动删除了这个定时任务,就不添加了 */
    {
      /* 重新计算超时时间 */
      it->second->restart(now);
      /* 重新添加到set中 */
      insert(it->second);
    }
    else
    {
      // FIXME move to a free list
      delete it->second; // FIXME: no delete please
    }
  }

  /* 计算下次timerfd被激活的时间 */
  if (!timers_.empty())
  {
    nextExpire = timers_.begin()->second->expiration();
  }

  /* 设置 */
  if (nextExpire.valid())
  {
    resetTimerfd(timerfd_, nextExpire);
  }
}

muduo将超时事件转换成文件描述符的可读事件,统一到io复用函数中,达到统一的效果。另外并没有将所有的定时任务都创建一个timerfd而是取最早超时的那个时间作为timerfd的超时时间,一方面减少内存,节省描述符,另一方面更方便管理。不过需要记得对超时时间的更新,因为用户再次添加的定时任务的超时时间可能早于先前设置的时间


几个C++方面的知识点

  • std::lower_bound,找到第一个大于等于给定值的位置,返回迭代器
  • std::back_inserter,获取末尾位置的迭代器
  • std::move,移动语义,避免拷贝,可以和右值引用一起使用
  • std::bind,绑定函数指针和对象
  • std::function,创建函数对象类型

你可能感兴趣的:(muduo源码学习,muduo网络库源码分析)