1. 程式人生 > >muduo_net程式碼剖析之定時器

muduo_net程式碼剖析之定時器

1、Timer*類簡介

  1. 這裡涉及了3個類TimerId、Timer、TimerQueue,反映到實際使用,主要是EventLoop中的三個函式:runAt()、runAfter()、runEvery()。
  2. 簡單來說,TimerQueue是用來進行管理排程的; 而Timer是真正的超時事件(該Class中封裝了真正的超時回撥)
  3. 要知道Timer*怎麼與EventLoop結合起來,還是從一個示例開始:
    通過runAt()、runAfter()、runEvery()函式與EventLoop結合起來
void printTid()
{
  printf("pid = %d, tid = %d\n"
, getpid(), muduo::CurrentThread::tid()); printf("now %s\n", muduo::Timestamp::now().toString().c_str()); } void print(const char* msg) { printf("msg %s %s\n", muduo::Timestamp::now().toString().c_str(), msg); if (++cnt == 20) { g_loop->quit(); } } int main() { printTid(); muduo:
:EventLoop loop; g_loop = &loop; print("main"); loop.runAfter(1, boost::bind(print, "once1")); #if 1 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")); loop.runEvery(2, boost::
bind(print, "every2")); loop.runEvery(3, boost::bind(print, "every3")); #endif loop.loop(); print("main loop exits"); sleep(1); }

2、timerfd_* 和gettimeofday

2.1 使用timerfd_*系列函式來處理定時任務

timerfd是Linux為使用者程式提供的一個定時器介面。這個介面基於檔案描述符,通過檔案描述符的可讀事件進行超時通知,所以能夠被用於select/poll的應用場景。

(1) 先介紹兩個和時間相關的結構體,可以設定超時時間、超時的重複時間

struct itimerspec {
    struct timespec it_interval;  /* 之後的超時時間即每隔多長時間超時 */
    struct timespec it_value;     /* 定時器第一次超時時間 */
};
struct timespec {
    time_t tv_sec;                /* 秒*/
    long   tv_nsec;               /* 納秒 */
};

(2) 再介紹timefd_*相關的3個函式

作用:建立一個定時器描述符timerfd
int timerfd_create(int clockid, int flags); 
返回值:timerfd(檔案描述符)
引數:
  clockid指定時間型別,有兩個值:
    CLOCK_REALTIME :Systemwide realtime clock. 系統範圍內的實時時鐘
    CLOCK_MONOTONIC:以固定的速率執行,從不進行調整和復位 ,它不受任何系統time-of-day時鐘修改的影響
  flags:可以是0或者O_NONBLOCK/O_CLOEXEC(該fd在exec時不會被繼承下去)

作用:用來啟動或關閉有fd指定的定時器
int timerfd_settime(int fd, int flags, 
    const struct itimerspec *new_value, //超時時間
    struct itimerspec *old_value //掩碼時間
);
引數:
  fd:timerfd,有timerfd_create函式返回
  fnew_value:指定新的超時時間,設定new_value.it_value非零則啟動定時器,否則關閉定時器;如果new_value.it_interval為0,則定時器只定時一次,即初始那次,否則之後每隔設定時間超時一次
  old_value:不為null,則返回定時器這次設定之前的超時時間
  flags:1代表設定的是絕對時間;為0代表相對時間。

作用:用於獲得定時器距離下次超時還剩下的時間
int timerfd_gettime(int fd, struct itimerspec *curr_value);
如果呼叫時定時器已經到期,並且該定時器處於迴圈模式(設定超時時間時struct itimerspec::it_interval不為0),那麼呼叫此函式之後定時器重新開始計時。

2.2 使用gettimeofday來獲取當前時間

這個函式被封裝在Timestamp類中,簡單示意如下:

class Timestamp
{
    public:
        Timestamp():microSecondsSinceEpoch_(0){}
        Timestamp(uint_64 micro):microSecondsSinceEpoch_(mirco){}
        static Timestamp now(){
            struct timeval tv; 
            gettimeofday(&tv, NULL); //獲取當前時間
            int64_t seconds = tv.tv_sec;
            //該構造初始化了microSecsSinceEpoch_
            return Timestamp(seconds * kMicroSecondsPerSecond + tv.tv_usec);
        }
};

通過把時間進行微秒級別的量化從而方便對時間戳的比較。

3、Timer* 類的設計與實現

首先,是EventLoop類物件的初始化:可以看到,在建立EventLoop的時刻,將會為EventLoop物件建立一個定時器物件TimerQueue

EventLoop::EventLoop()
  : looping_(false),
    quit_(false),
    threadId_(CurrentThread::tid()),
    poller_(new Poller(this)),
    timerQueue_(new TimerQueue(this))
{
//...
}

然後,看TimerQueue的建構函式:因為TimerQueue類中有loop_成員,因此可以將TimerQueue物件新增到loop_中並進行監聽定時事件的到來

TimerQueue::TimerQueue(EventLoop* loop)
  : loop_(loop),
    timerfd_(createTimerfd()),//呼叫::timerfd_create建立了timefd描述符
    timerfdChannel_(loop, timerfd_),//使用timerfd_構造channel物件,並安插到loop上
    timers_()//儲存Timer的關鍵結構
{
  timerfdChannel_.setReadCallback( //設定回撥函式handleRead
      boost::bind(&TimerQueue::handleRead, this));
  timerfdChannel_.enableReading(); //監聽讀事件
}

當timerfd_上的讀事件到來時,將會觸發回撥函式TimerQueue::handleRead

void TimerQueue::handleRead()
{
  loop_->assertInLoopThread();
  Timestamp now(Timestamp::now());
  readTimerfd(timerfd_, now);

  std::vector<Entry> expired = getExpired(now);//獲取超時的事件

  //依次呼叫Timer中的回撥。
  for (std::vector<Entry>::iterator it = expired.begin();
      it != expired.end(); ++it)
  {
    it->second->run(); //呼叫回撥函式
  }
  //執行完一次超時回撥後,這個時候需要重新設定定時器,把當前未過期的最早時間作為定時器最新時間
  reset(expired, now);
}

建構函式幫我們做了很多事情,不過上述情況是假設TimerQueue中已經有一系列已經排好序列的時間事件了,現在要來看看怎麼新增定時器(即新增定時器的策略)

對應EventLoop中的函式就是runAt、runAfter、runEvery
其實主要是TimeQueue::addTimer函式,因為runAfter和runEvery都是通過設定不同的引數去呼叫TimeQueue::addTimer。

TimerId TimerQueue::addTimer(const TimerCallback& cb,//超時回撥
                             Timestamp when,//超時時間
                             double interval)//重複時間
{
  Timer* timer = new Timer(cb, when, interval);//構造Timer
  loop_->assertInLoopThread();
  
  //把當前未過期的最早時間設定為定時器的超時時間
  bool earliestChanged = insert(timer);
  if (earliestChanged)
  {
    resetTimerfd(timerfd_, timer->expiration());
  }
  return TimerId(timer);
}

綜上所述,整個過程如下:
在這裡插入圖片描述

4、 Timer*類的原始碼分析

TimerId類

TimerId非常簡單,它被設計用來取消Timer的,它的結構很簡單,只有一個Timer指標和其序列號

class TimerId : public muduo::copyable
{
public:
  TimerId()
    : timer_(NULL),
      sequence_(0)
  {
  }

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

  friend class TimerQueue; //TimerQueue是TimerId的友元類,可以訪問TimerId的私有函式

private:
  Timer* timer_; //Timer指標
  int64_t sequence_; //序列號
};

Timer類

  1. 超時時間/重複時間間隔、定時器是否重複、定時器序列號、超時回撥函式
  2. run()呼叫回撥函式、restart用來重啟定時器(如果repeat_)
class Timer : noncopyable
{
 public:
  Timer(TimerCallback cb, Timestamp when, double interval)
    : callback_(std::move(cb)), //回撥函式
      expiration_(when), //一次的超時時刻
      interval_(interval), //如果重複,間隔時間(超時時間間隔,如果是一次性定時器,該值為0)
      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) //重新設定超時時間
  {
    if (repeat_)//如果設定重複,則重新新增
    {
      expiration_ = addTime(now, interval_);//將now和interval_相加,重新設定超時時間
    }
    else //不重複設定
    {
      expiration_ = Timestamp::invalid(); //設為無效時間
    }
  }

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

 private:
  const TimerCallback callback_; //超時回撥函式
  Timestamp expiration_;  //下一次的超時時刻
  const double interval_; //超時時間間隔,如果是一次性定時器,該值為0
  const bool repeat_; //是否重複
  const int64_t sequence_; //定時器序號

  static AtomicInt64 s_numCreated_;//定時器計數,當前已經建立的定時器數量
};

TimerQueue類

雖然TimerQueue中有Queue,但是其實現時基於Set的,而不是Queue。這樣可以高效地插入、刪除定時器,且找到當前已經超時的定時器。TimerQueue的public介面只有兩個,新增和刪除。

  1. TimerQueue的封裝是為了讓未到期的時間Timer有序的排列起來,這樣,能夠根據當前時間找到已經到期的Timer也能高效的新增和刪除Timer。
  2. 到期的時間應該被清除去執行相應的回撥,未到期的時間則應該有序的排列起來

總結定時器的使用方法

  1. 使用muduo庫中封裝好的定時器
    EventLoop中使用runAt()、runAfter()、runEvery()新增定時器
  2. 手動建立定時器,詳細過程見下
    (1) timerfd_create建立事件timerfd
    (2) 用timerfd構造Channel物件
    (3) Channel設定回撥函式、啟用讀事件
    (4) timerfd_settime為timerfd設定超時時間
    (5) loop.loop()進行監聽超時時間
    在這裡插入圖片描述