基於epoll的定時器


http://blog.csdn.net/voidccc/article/details/8878967

http://www.cnblogs.com/my_life/articles/5253175.html

http://www.cnblogs.com/my_life/articles/5216593.html

 

1 原書中對Timer的介紹分布在兩個地方,<<7.8定時器>>和<<8.2 TimerQueue定時器>>,作者在7.82節總結了選擇timerfd來作為多線程服務器程序的定時器的原因:

    1 sleep(3)/alarm(2)/usleep(3)在實現時有可能用了SIGALRM信號, 在多線程程序中處理信號是個相當麻煩的事情,應當盡量避免

    2 nanosleep(2)和clock_nanosleep(2)是線程安全的,但是在非阻塞網絡編程中,絕對不能用讓線程掛起的方式來等待一段時間,這樣一來程序會失去響應。正確的做法是注冊一個時間回調函數。

    3 getitimer(2)和timer_create(2)也是用信號來deliver超時,在多線程程序中也會有麻煩

    4 timerfd_create(2)把時間變成了一個文件描述符,該文件描述符在定時器超時的那一刻變得可讀,這樣就能很方便的融入select(2)/poll(2)框架中,用統一的方式來處理IO時間和超時事件。

    5 傳統的Reactor利用select(2)/poll(2)/epoll(4)/的timeout來實現定時功能(時間得排序,每次epoll_wait(最小的超時時間還剩下的時間)),

  但poll(2)/和epoll_wait(2)的定時精度只有毫秒,遠低於timerfd_settime(2)的定時精度。

2 有必要先看看最進本的timerfd是如何工作的,

注意下面粘貼代碼/行數都是mini-muduo里的,而非muduo,當然了muduo的實現原理是一樣的。 https://github.com/voidccc/mini-muduo 

在我們的系統里,使用timerfd作為定時器,實際用到了下面5個函數,其中前兩個為timer文件描述專用,后面三個可以用在多種文件描述符上。

 

 
  1. int timerfd_create(int clockid, int flags) //創建一個定時器文件  
  2. int timerfd_settime(int ufd, int flags, const struct itimerspec * utmr, struct itimerspec * otmr); //設置新的超時時間,並開始計時  
  3. int epoll_ctl(_epollfd, EPOLL_CTL_ADD, fd, &ev) //將timer文件描述符加入到epoll檢測  
  4. int epoll_wait(_epollfd, _events, MAX_EVENTS, -1); //在epoll上等待各種文件描述符事件  
  5. int close(int fd); //釋放掉文件描述符  

這5個函數列出的順序正好也是實際使用定時器過程中調用的順序。首先通過timerfd_create創建一個Timer文件描述符,然后通過timerfd_settime來設置超時時間,之后將Timer文件描述符加入到epoll的檢測,程序通過一個循環等待在epoll_wait上,因為沒有Timer到時而導致阻塞。一旦定時器到時,epoll_wait就會返回,我們就可以進行相關處理。(在muduo/mini-muduo里,注冊到epoll描繪符和接收epoll_wait()通知都是通過Channel來實現的)

 

3 看用戶是如何使用Timer的,用戶要使用網絡庫的定時器,必須通過EventLoop。EventLoop有三個接口暴露了Timer相關的操作

 

 
  1. int runAt(Timestamp when, IRun* pRun); //在指定的某個時刻調用函數  
  2. int runAfter(double delay, IRun* pRun); //等待一段時間后,調用函數  
  3. int runEvery(double interval, IRun* pRun); //以固定的時間間隔反復調用函數  
  4. void cancelTimer(int timerfd); //關閉一個Timer  

 

前3個函數的返回值int是用來唯一確定一個定時器的ID,當需要關閉某個Timer的時候,將ID傳遞給cancelTimer函數即可。IRun是一個回調接口,在mini-muduo里幾乎所有的回調都是通過IRun來完成的。runAt的第一個參數是一個代表時間的Timestamp對象,runAfter和runEvery的第一個參數都是以秒為單位的。

4 詳細分析下Timer是怎么實現的,其實EventLoop里只有一個Timer文件描述符,當用戶通過上面的3個接口向EventLoop添加的所有定時器,實際都工作在同一個timerfd上,這個是怎么做到的呢?我們來跟蹤一下EventLoop::runAt()的實現

 

 
  1. 100 int EventLoop::runAt(Timestamp when, IRun* pRun)  
  2. 101 {     
  3. 102     return _pTimerQueue->addTimer(pRun, when, 0.0);  
  4. 103 }  

 

EventLoop::runAt(...)直接調用了TimerQueue::addTimer()

 
  1. 66 int TimerQueue::addTimer(IRun* pRun, Timestamp when, double interval)  
  2. 67 {  
  3. 68     Timer* pTimer = new Timer(when, pRun, interval); //Memory Leak !!!  
  4. 69     _pLoop->queueLoop(_addTimerWrapper, pTimer);  
  5. 70     return (int)pTimer;  
  6. 71 }  

這里面新建了一個Timer對象,然后就調用了EventLoop::queueLoop(...),而queueLoop方法的作用就是異步執行(目前只有一個線程,所以只有異步執行的功能)。異步執行了TimerQueue::doAddTimer(...)方法。再來看看doAddTimer方法

 

 
  1. 33 void TimerQueue::doAddTimer(void* param)                                     
  2. 34 {     
  3. 35     Timer* pTimer = static_cast<Timer*>(param);                              
  4. 36     bool earliestChanged = insert(pTimer);                                   
  5. 37     if(earliestChanged)                                                      
  6. 38     {     
  7. 39         resetTimerfd(_timerfd, pTimer->getStamp());                          
  8. 40     }                                                                        
  9. 41 }   

這里調用了兩個方法,一個是insert(...),一個是resetTimerfd(...),在insert(...)里,程序將Timer插入到一個TimerList里,也就是一個std::set<std::pair<Timestamp, Timer*>>,看上去有點繁瑣,其實是一個 時間->Timer 鍵值對的set,這個set的目的是存放所有未到期的定時器,每當timerfd到時,就從里面取出最近的一個定時器,然后修改timerfd把定時器修改成set里下一個最近的時間,這樣實現了只使用一個timerfd來管理多個定時器的功能。insert的返回值是個布爾型,意義是新加入的這個定時器是否否比整個set里所有定時器發生的還要早,如果是的話,就必須立刻修改timerfd,將timerfd的定時時間改成這個最近的定時器。如果新加入的定時器不是set里最先發生的定時器,則不用修改timerfd了。

 

 
  1. 163 bool TimerQueue::insert(Timer* pTimer)  
  2. 164 {  
  3. 165     bool earliestChanged = false;  
  4. 166     Timestamp when = pTimer->getStamp();  
  5. 167     TimerList::iterator it = _timers.begin();  
  6. 168     if(it == _timers.end() || when < it->first)  
  7. 169     {  
  8. 170         earliestChanged = true;  
  9. 171     }  
  10. 172     pair<TimerList::iterator, bool> result  
  11. 173        = _timers.insert(Entry(when, pTimer));  
  12. 174     if(!(result.second))  
  13. 175     {  
  14. 176         cout << "_timers.insert() error " << endl;  
  15. 177     }  
  16. 178   
  17. 179     return earliestChanged;  
  18. 180 }  
[cpp]  view plain  copy
 
 print?
  1. 149 void TimerQueue::resetTimerfd(int timerfd, Timestamp stamp)  
  2. 150 {  
  3. 151     struct itimerspec newValue;  
  4. 152     struct itimerspec oldValue;  
  5. 153     bzero(&newValue, sizeof(newValue));  
  6. 154     bzero(&oldValue, sizeof(oldValue));  
  7. 155     newValue.it_value = howMuchTimeFromNow(stamp);  
  8. 156     int ret = ::timerfd_settime(timerfd, 0, &newValue, &oldValue);  
  9. 157     if(ret)  
  10. 158     {  
  11. 159         cout << "timerfd_settime error" << endl;  
  12. 160     }  
  13. 161 }  

幾個實現細節:

    細節1:注意定時器容器的選擇,也就是TimerQueue里的_timers成員變量,muduo選擇了使用二叉搜索樹,也就是std::map或者std::set,這兩者二選一的過程中,作者最終選擇了set,因為同一個時間點下可能有多個定時器,所以直接使用map是不合適的,因為一個pair<Timestamp, Timer*>代表着一個時間,如果直接用map<Timestamp, *Timer>只能保證一個時間點下只對應一個Timer*,這不符合要求,所以作者使用了稍微復雜的類型set<pair<Timestamp, Timer*>> 作為存儲定時器的容器。mini-muduo在返回值方面進行了簡化,沒有像muduo一樣返回一個TimerId,而是直接返回了int,其實就是Timer對象的地址。由於在同一個進程里,對象地址是不可能相同的,這樣就簡單粗暴解決了同樣時間下多個定時器的問題。

    細節2:定時器的插入操作,使用insert加入到set后,就自動按照Timestamp排序了,Iterator的begin的first就是所有定時器里最早的那一個。如果要插入新的定時器,有個重要的標志,就是earliestTimerChanged,重要條件作為判斷,如果(it == end || when < it.begin.first)條件滿足就代表標志改變了。

    細節3:注意Timestamp的運算符重載

 

[cpp]  view plain  copy
 
 print?
  1. 57 bool operator<(Timestamp l, Timestamp r)  
  2. 58 {  
  3. 59     return l.microSecondsSinceEpoch() < r.microSecondsSinceEpoch();  
  4. 60 }  
  5. 61   
  6. 62 bool operator==(Timestamp l, Timestamp r)  
  7. 63 {  
  8. 64     return l.microSecondsSinceEpoch() == r.microSecondsSinceEpoch();  
  9. 65 }  

 

    細節4:Timestamp::tostring()方法,要打印64位整數,在32/64平台上可以使用下面的方法

 

[cpp]  view plain  copy
 
 print?
  1. #include <inttypes.h>  
  2. //跨平台打印方法  
  3. printf("%" PRId64 "\n", value);  
  4. // 相當於64位的:  
  5. printf("%" "ld" "\n", value);  
  6. // 或32位的:  
  7. printf("%" "lld" "\n", value);  

注意要使用PRId64,還要做個小處理,使用宏來包裹#include<inttypes.h>,所以Timestamp里的代碼就寫成了下面的樣子,有點奇怪。

[cpp]  view plain  copy
 
 print?
  1. 5 #define __STDC_FORMAT_MACROS  
  2. 6 #include <inttypes.h>  
  3. 7 #undef __STDC_FORMAT_MACROS  

關於__STDC_FORMAT_MACROS宏的問題,可以閱讀下后面這篇文章http://blog.163.com/guixl_001/blog/static/4176410420121021111117987/

 

    細節5:Interval和expiration的名字應該是來源於man timerfd_settime的i解釋
    細節6:修改IRun接口,原有的不滿足要求,因為不能保存調用參數。同時由於修改了IRun接口,所以在EvenrLoop里添加了Runner類,用於存儲回調和參數,原始的_pendingFunctors只能存儲回調,參數沒地方存。
    細節7:由於還沒太讀懂muduo的cancelTimers隊列所以本版本暫時不實現了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM