如何使用C++11實現跨平台的定時器timer?

定時器要能夠在指定的時間非同步執行一個操作,比如boost庫中的boost::asio::deadline_timer,或MFC中的定時器。而且能夠被取消執行。

最基本的,能夠在x86和x64架構上的Win、Mac和Linux操作系統下實現。


一個Timer的實現需要具備以下幾個行為:

  • StartTimer(Interval, ExpiryAction)

註冊一個時間間隔為 Interval 後執行 ExpiryAction 的定時器實例,其中,返回 TimerId 以區分在定時器系統中的其他定時器實例。

  • StopTimer(TimerId)

根據 TimerId 找到註冊的定時器實例並執行 Stop 。

  • PerTickBookkeeping()

在一個 Tick 時間粒度內,定時器系統需要執行的動作,它最主要的行為,就是檢查定時器系統中,是否有定時器實例已經到期。

具體的代碼實現思路就是:
在StartTimer的時候,把 當前時間 + Interval 作為key放入一個容器,然後在Loop的每次Tick里,從容器裡面選出一個最小的key與當前時間比較,如果key小於當前時間,則這個key代表的timer就是expired,需要執行它的ExpiryAction(一般為回調)。

這裡有兩個實現的細節:

  1. 獲取當前時間

這可能是實現一個定時器唯一需要關心平台差異的地方,需要注意的地方包括時間精度,使用系統開啟後的CPU時間還是壁鍾時間,常用的API是:
Windows: QueryPerformanceFrequency() 和 QueryPerformanceCounter()
Linux: clock_gettime()
OSX: gettimeofday()或者mach_absolute_time()
當然在C++11里也可以偷懶使用chrono的high_resolution_clock std::chrono::high_resolution_clock

2. Timer容器的選擇

容器應該能夠在很短的時間內找到MinValue
最小堆的find-min複雜度是O(1),所以蠻受人喜歡的
STL里提供有堆的API,make_heap, push_heap, pop_heap, sort_heap

或者直接使用std::priority_queue(std::priority_queue - cppreference.com)

3. PerTickBookkeeping是放在主循環線程還是另起線程

放在主循環的話的套路大都是基於回調函數的單線程實現,另起線程需要想好線程間通信的方式,asio和skynet有單獨的timer線程

4,一些開源項目里的timer實現

a) 這是boost.asio的實現的timer_queue,用的是最小堆
asio/timer_queue.hpp at master · chriskohlhoff/asio · GitHub

b) 這是libuv的timer,windows下採用的是紅黑樹:

libuv/timer.c at v1.x · libuv/libuv · GitHub

linux下還是最小堆:

https://github.com/libuv/libuv/blob/v1.x/src/unix/timer.c

c) 這是linux kernel里的實現,經典的多級鏈表時間輪

https://github.com/torvalds/linux/blob/master/kernel/time/timer.c

時間輪的實現原理可以參考這裡: [轉載]linux 內核定時器詳解

另外,也有其它著名的開源項目採用了時間輪的方式來實現定時器,

比如,雲風的skynet skynet/skynet_timer.c at master · cloudwu/skynet · GitHub

facebook的folly裡面的HHWheelTimer https://github.com/facebook/folly/blob/master/folly/io/async/HHWheelTimer.h

參考:

Linux 下定時器的實現方式分析


謝邀~MFC或者說win32 api的計時器是在windows message loop里的,可以看成main里有個for(;;){} 無限循環,掐時間觸發計時器事件。因為同時還要處理其他窗口事件,精度不高,但處於主線程中,方便調用其他窗口函數。沒用過boost的定時器,不清楚。不過可以按上面的想法實現一個跨平台的計時器。


試試libevent中的定時器?


// 實現1秒的定時器 滿足2點
1, 定時觸發(wait out time)
2, 隨時退出(notify condition)
unique_lock& sleepLock(sleepMutex);
condition.wait_for(sleepLock, std::chrono::milliseconds(1000));


這裡有些例子還不錯,有最小堆和時間輪的:高性能伺服器開發之C++定時器


GitHub - pp-qq/timer: C++ 定時器, 基於 C++11, libuv 實現, 實現思路上和上面幾位差不多. 滿足問題中跨平台, 定時器可以被取消等要求.

具體實現細節就是, 在調用 `TimerManager::Start()` 之後會創建 `thread_num` 個工作線程, 每一個工作線程中都運行著一個 uv loop. 當通過 `TimerManager::StartTimer()` 提交一個定時器時, 會通過 round-robin 演算法選擇一個工作線程(下稱 P), 然後將定時器請求交給 P. P 在收到定時器請求之後會根據請求細節創建一個 `uv_timer_t` 對象來實現定時機制.

這有一個栗子

struct TimerCB {
int timer_id = 0;
int times = 0;
int total_times = 5;

public:
TimerCB(int id, int total_times_arg) noexcept :
timer_id(id),
total_times(total_times_arg) {
LOG(INFO) &<&< "id: " &<&< timer_id &<&< "; times: " &<&< times &<&< "; total_times: " &<&< total_times; } unsigned int operator()(unsigned int status) noexcept { LOG(INFO) &<&< "id: " &<&< timer_id &<&< "; times: " &<&< times &<&< "; total_times: " &<&< total_times &<&< "; status: " &<&< status; if (++times &>= total_times)
return TimerManager::kStopTimer; // 取消定時器
return 0;
}
};

int main(int argc, char **argv) {
TimerManager timer_manager;
timer_manager.thread_num = FLAGS_work_thread_num;
timer_manager.Start();

timer_manager.StartTimer(0, 0, TimerCB(1, 3));
timer_manager.StartTimer(3000, 0, TimerCB(2, 3));
timer_manager.StartTimer(0, 2000, TimerCB(3, 3));
timer_manager.StartTimer(1000, 2000, TimerCB(4, 3));

timer_manager.Join();
}

執行結果如下:

$ grep "id: 1" /tmp/test.INFO
I1007 15:53:20.716138 9764 main.cc:18] id: 1; times: 0; total_times: 3
I1007 15:53:20.716989 9765 main.cc:22] id: 1; times: 0; total_times: 3; status: 0
I1007 15:53:20.717087 9765 main.cc:22] id: 1; times: 1; total_times: 3; status: 1
$ grep "id: 2" /tmp/test.INFO
I1007 15:53:20.716536 9764 main.cc:18] id: 2; times: 0; total_times: 3
I1007 15:53:23.720160 9766 main.cc:22] id: 2; times: 0; total_times: 3; status: 0
I1007 15:53:23.720175 9766 main.cc:22] id: 2; times: 1; total_times: 3; status: 1
$ grep "id: 3" /tmp/test.INFO
I1007 15:53:20.716635 9764 main.cc:18] id: 3; times: 0; total_times: 3
I1007 15:53:20.716866 9767 main.cc:22] id: 3; times: 0; total_times: 3; status: 0
I1007 15:53:22.719032 9767 main.cc:22] id: 3; times: 1; total_times: 3; status: 0
I1007 15:53:24.721151 9767 main.cc:22] id: 3; times: 2; total_times: 3; status: 0
I1007 15:53:24.721211 9767 main.cc:22] id: 3; times: 3; total_times: 3; status: 1
$ grep "id: 4" /tmp/test.INFO
I1007 15:53:20.716729 9764 main.cc:18] id: 4; times: 0; total_times: 3
I1007 15:53:21.717957 9768 main.cc:22] id: 4; times: 0; total_times: 3; status: 0
I1007 15:53:23.720064 9768 main.cc:22] id: 4; times: 1; total_times: 3; status: 0
I1007 15:53:25.722184 9768 main.cc:22] id: 4; times: 2; total_times: 3; status: 0
I1007 15:53:25.722245 9768 main.cc:22] id: 4; times: 3; total_times: 3; status: 1


非同步執行的話另開定時器線程loop sleep就好了,使用堆或者set存儲。如果是同步的話則可以在主循環里定時檢查,Linux下則有更好用的timer fd或者alarm。


推薦閱讀:

在 Windows 上不用 Win32 API 可以繪製出一個窗口么?
我用的是visual studio 2010 c語言為什麼學了好長時間還是控制台程序和dos窗口啊?
為什麼使用virtual關鍵字在C++與C#會出現不同的效果?求解答。
使用visual studio 2012編寫每一個c程序是都必須新建工程嗎?
我是一個物聯網新生,是先學C語言還是C++?

TAG:CC | 多線程編程 | C11 |