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入選的原因
timerfd*_入選的原因
select/poll/epoll可以設置timeout來實現超時,但是poll/epoll的精度只有毫秒,遠低于timerfd_settime
---------------------------------------摘自《Linux多線程服務器編程》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);timerfd_*系列函數
#include <sys/timerfd.h> 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<Timerstamp, Timer*>作為std::set的鍵,通過比較超時時間來排序(std::set內部采用紅黑樹,一種二叉搜索樹)
對于定時任務的原理,
整個過程只有一個timerfd被Poller監聽,所以調用timerfd_settime設置的超時時間一定是TimerQueue的set里最小的,即set.begin();第一個Timer任務。
而用戶是通過調用EventLoop::runAt/runAfter/runEvery函數注冊定時任務的,這些函數都需要向TimerQueue的set中添加Timer,所以每添加一個都需要判斷新添加的定時任務的超時時間是否小于設置的超時時間,如果小于,就需要調用timerfd_settime重新設置timerfd的超時時間。
而每次timerfd被激活都需要找到在set中所有的超時任務,因為有可能存在超時時間相等的定時任務,可以使用std::lower_bound函數找到第一個大于等于給定值的位置
TimerQueue由所在的EventLoop持有,用戶設置定時任務也是調用的EventLoop的接口,進而調用TimerQueue的接口
EventLoop提供三個接口用于注冊定時任務,進而調用TimerQueue的addTimer接口
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<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;typedef std::set<Entry> TimerList;/* * 主要用于刪除操作,通過TimerId找到Timer*,再通過Timer*找到在timers_中的位置,將期刪除* 覺得可以省略*/typedef std::pair<Timer*, int64_t> ActiveTimer;typedef std::set<ActiveTimer> 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<Entry> getExpired(Timestamp now);/* 將超時任務中周期性的任務重新添加到timers_中 */void reset(const std::vector<Entry>& 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, Timer*> */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::pair<TimerList::iterator, bool> result= timers_.insert(Entry(when, timer));assert(result.second); (void)result;}{/* 同時也添加到activeTimers_中,用于刪除時查找操作 */std::pair<ActiveTimerSet::iterator, bool> 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<Entry> expired = getExpired(now);callingExpiredTimers_ = true;cancelingTimers_.clear();// safe to callback outside critical section/* 調用超時的事件回調函數 */for (std::vector<Entry>::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中,不過記得要重新計算超時時間
/* * 調用完所有超時的回調函數后,需要對這些超時任務進行整理* 將周期性的定時任務重新添加到set中*/ void TimerQueue::reset(const std::vector<Entry>& expired, Timestamp now) {Timestamp nextExpire;for (std::vector<Entry>::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 listdelete 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网络库学习(三)定时器TimerQueue的设计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: muduo网络库学习(二)对套接字和监听
- 下一篇: muduo网络库学习(四)事件驱动循环E