程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> Muduo網絡編程示例之三:定時器

Muduo網絡編程示例之三:定時器

編輯:關於C語言

程序中的時間
程序中對時間的處理是個大問題,我打算單獨寫一篇文章來全面地討論這個問題。文章暫定名《〈程序中的日期與時間〉第二章 計時與定時》,跟《〈程序中的日期與時間〉第一章 日期計算》放到一個系列,這個系列預計會有四篇文章。

在這篇博客裡裡我先簡要談談與編程直接相關的內容,把更深入的內容留給上面提到的日期與時間專題文章。

在一般的服務端程序設計中,與時間有關的常見任務有:

獲取當前時間,計算時間間隔;
時區轉換與日期計算;把紐約當地時間轉換為上海當地時間;2011-02-05 之後第 100 天是幾月幾號星期幾?等等
定時操作,比如在預定的時間執行一項任務,或者在一段延時之後執行一項任務。
其中第 2 項看起來復雜,其實最簡單。日期計算用 Julian Day Number,時區轉換用 tz database;惟一麻煩一點的是夏令時,但也可以用 tz database 解決。這些操作都是純函數,很容易用一套單元測試來驗證代碼的正確性。需要特別注意的是,用 tzset/localtime_r 來做時區轉換在多線程環境下可能會有問題;對此我的解決辦法是寫一個 TimeZone class,以避免影響全局,將來在日期與時間專題中會講到。以下本文不考慮時區,均為 UTC 時間。

真正麻煩的是第 1 項和第 3 項。一方面,Linux 有一大把令人眼花缭亂的與時間相關的函數和結構體,在程序中該如何選用?另一方面,計算機中的時鐘不是理想的計時器,它可能會漂移或跳變;最後,民用的 UTC 時間與閏秒的關系也讓定時任務變得復雜和微妙。當然,與系統當前時間有關的操作也讓單元測試變得困難。

Linux 時間函數
Linux 的計時函數,用於獲得當前時間:

time(2) / time_t (秒)
ftime(3) / struct timeb (毫秒)
gettimeofday(2) / struct timeval (微秒)
clock_gettime(2) / struct timespec (納秒)
gmtime / localtime / timegm / mktime / strftime / struct tm (這些與當前時間無關)
定時函數,用於讓程序等待一段時間或安排計劃任務:

sleep
alarm
usleep
nanosleep
clock_nanosleep
getitimer / setitimer
timer_create / timer_settime / timer_gettime / timer_delete
timerfd_create / timerfd_gettime / timerfd_settime
我的取捨如下:

(計時)只使用 gettimeofday 來獲取當前時間。
(定時)只使用 timerfd_* 系列函數來處理定時。
gettimeofday 入選原因:(這也是 muduo::Timestamp class 的主要設計考慮)

time 的精度太低,ftime 已被廢棄,clock_gettime 精度最高,但是它系統調用的開銷比 gettimeofday 大。
在 x86-64 平台上,gettimeofday 不是系統調用,而是在用戶態實現的(搜 vsyscall),沒有上下文切換和陷入內核的開銷。
gettimeofday 的分辨率 (resolution) 是 1 微秒,足以滿足日常計時的需要。muduo::Timestamp 用一個 int64_t 來表示從 Epoch 到現在的微秒數,其范圍可達上下 30 萬年。
timerfd_* 入選的原因:

sleep / alarm / usleep 在實現時有可能用了信號 SIGALRM,在多線程程序中處理信號是個相當麻煩的事情,應當盡量避免。(近期我會寫一篇博客仔細講講“多線程、RAII、fork() 與信號”)
nanosleep 和 clock_nanosleep 是線程安全的,但是在非阻塞網絡編程中,絕對不能用讓線程掛起的方式來等待一段時間,程序會失去響應。正確的做法是注冊一個時間回調函數。
getitimer 和 timer_create 也是用信號來 deliver 超時,在多線程程序中也會有麻煩。timer_create 可以指定信號的接收方是進程還是線程,算是一個進步,不過在信號處理函數(signal handler)能做的事情實在很受限。
timerfd_create 把時間變成了一個文件描述符,該“文件”在定時器超時的那一刻變得可讀,這樣就能很方便地融入到 select/poll 框架中,用統一的方式來處理 IO 事件和超時事件,這也正是 Reactor 模式的長處。我在一年前發表的《Linux 新增系統調用的啟示》中也談到這個想法,現在我把這個想法在 muduo 網絡庫中實現了。
傳統的 Reactor 利用 select/poll/epoll 的 timeout 來實現定時功能,但 poll 和 epoll 的定時精度只有毫秒,遠低於 timerfd_settime 的定時精度。
必須要說明,在 Linux 這種非實時多任務操作系統中,在用戶態實現完全精確可控的計時和定時是做不到的,因為當前任務可能會被隨時切換出去,這在 CPU 負載大的時候尤為明顯。但是,我們的程序可以盡量提高時間精度,必要的時候通過控制 CPU 負載來提高時間操作的可靠性,在程序在 99.99% 的時候都是按預期執行的。這或許比換用實時操作系統並重新編寫並測試代碼要經濟一些。

關於時間的精度(accuracy)問題我留到專題博客文章中討論,它與分辨率(resolution)不完全是一回事兒。時間跳變和閏秒的影響與應對也不在此處展開討論了。

Muduo 的定時器接口
Muduo EventLoop 有三個定時器函數:

   1: typedef boost::function<void()> TimerCallback;   2:     3: ///   4: /// Reactor, at most one per thread.   5: ///   6: /// This is an interface class, so dont expose too much details.   7: class EventLoop : boost::noncopyable   8: {   9:  public:  10:   // ...  11:    12:   // timers  13:    14:   ///  15:   TimerId runAt(const Timestamp& time, const TimerCallback& cb);  16:    17:   ///  18:   /// Runs callback after @c delay seconds.  19:   /// Safe to call from other threads.  20:   TimerId runAfter(double delay, const TimerCallback& cb);  21:    22:   ///  23:   /// Runs callback every @c interval seconds.  24:   /// Safe to call from other threads.  25:   TimerId runEvery(double interval, const TimerCallback& cb);  26:    27:   /// Cancels the timer.  28:   /// Safe to call from other threads.  29:   // void cancel(TimerId timerId);  30:    31:   // ...  32: };
runAt 在指定的時間調用 TimerCallback
runAfter 等一段時間調用 TimerCallback
runEvery 以固定的間隔反復調用 TimerCallback
cancel 取消 timer,目前未實現
回調函數在 EventLoop 對象所在的線程發生,與 onMessage() onConnection() 等網絡事件函數在同一個線程。

Muduo 的 TimerQueue 采用了最簡單的實現(鏈表)來管理定時器,它的效率比不上常見的 binary heap 的做法,如果程序中大量(10 個以上)使用重復觸發的定時器,或許值得考慮改用更高級的實現。我目前還沒有在一個程序裡用過這麼多定時器,暫時也不打算優化 TimerQueue。

Boost.Asio Timer 示例
Boost.Asio 教程裡以 Timer 和 Daytime 為例介紹 asio 的基本使用,daytime 已經在前文“示例一”中介紹過,這裡著重談談 Timer。Asio 有 5 個 Timer 示例,muduo 把其中四個重新實現了一遍,並擴充了第 5 個示例。

阻塞式的定時,muduo 不支持這種用法,無代碼。
非阻塞定時,見 examples/asio/tutorial/timer2
在 TimerCallback 裡傳遞參數,見 examples/asio/tutorial/timer3
以成員函數為 TimerCallback,見 examples/asio/tutorial/timer4
在多線程中回調,用 mutex 保護共享變量,見 examples/asio/tutorial/timer5
在多線程中回調,縮小臨界區,把不需要互斥執行的代碼移出來,見 examples/asio/tutorial/timer6
為節省篇幅,這裡只列出 timer4:

   1: #include    2:     3: #include    4: #include    5: #include    6:     7: class Printer : boost::noncopyable   8: {   9:  public:  10:   Printer(muduo::net::EventLoop* loop)  11:     : loop_(loop),  12:       count_(0)  13:   {  14:     loop_->runAfter(1, boost::bind(&Printer::print, this));  15:   }  16:    17:   ~Printer()  18:   {  19:     std::cout << "Final count is " << count_ << " ";  20:   }  21:    22:   void print()  23:   {  24:     if (count_ < 5)  25:     {  26:       std::cout << count_ << " ";  27:       ++count_;  28:    29:       loop_->runAfter(1, boost::bind(&Printer::print, this));  30:     }  31:     else  32:     {  33:   &n

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved