Linux网络编程 | 高性能定时器 :时间轮、时间堆
文章目錄
- 時間輪
- 時間堆
在上一篇博客中我實現了一個基于排序鏈表的定時器容器,但是其存在一個缺點——隨著定時器越來越多,添加定時器的效率也會越來越低。
而下面的兩個高效定時器——時間輪、時間堆,會完美的解決這個問題。
時間輪
為了解決排序鏈表的缺點,時間輪運用了哈希的思想,將定時器散列到不同的鏈表上,這樣每條鏈表上的定時器數目就明顯的少于全部存在一條鏈表上的情況,此時的插入操作效率就不會收到定時器數量的影響
在圖上所示的時間輪中,指針每次會指向輪子上的一個槽,并且會順時針移動,每次移動則會指向下一個槽。其中每次移動的時間間隔就是心搏時間si,由于時間輪有N個槽,所以轉動一周的時間就是N * si。
每一個槽都指向一個鏈表,每條鏈表之間設定的定時時間都是si的整數倍,通過利用這個關系來將定時器映射到不同的槽中。并且由于結構呈換環狀,即使定時時間大于一圈N * si,也只需要讓其在對應轉動的圈數生效即可。
移動的槽數 = 定時時間 / 轉動間隔 插入槽 = (當前槽 + (移動的槽數 % 槽總數) % 槽總數);所以對于時間輪來說,要提高定時精度就需要使得轉動間隔足夠小。而要提高效率則要求槽數盡量的多。
#ifndef __TIMER_WHEEL_H__ #define __TIMER_WHEEL_H__#include<time.h> #include<stdio.h> #include<netinet/in.h>const int MAX_BUFFER_SIZE = 1024; const int SLOT_COUNT = 60; const int SLOT_INERTVAL = 1;class tw_timer;//用戶數據 struct client_data {sockaddr_in addr;int sock_fd;char buff[MAX_BUFFER_SIZE];tw_timer* timer; };//定時器類 class tw_timer { public:tw_timer(int rot, int ts): _rotation(rot), _time_slot(ts), _next(nullptr), _prev(nullptr){}int _rotation; //旋轉的圈數int _time_slot; //記錄在哪一個槽中void (*fun)(client_data*); //處理函數client_data* _user_data; //用戶參數tw_timer* _next;tw_timer* _prev; };//定時器鏈表,帶頭尾雙向鏈表,定時器以升序排序 class timer_wheel { public:timer_wheel(): cur_slot(0){//初始化每個槽的頭節點for(int i = 0; i < SLOT_COUNT; i++){_slots[i] = nullptr;}}~timer_wheel(){for(int i = 0; i < SLOT_COUNT; i++){//刪除每一個槽中的所有節點tw_timer* cur = _slots[i];while(cur){tw_timer* next = cur->_next;delete cur;cur = next;}} }//防拷貝timer_wheel(const timer_wheel&) = delete;timer_wheel& operator=(const timer_wheel&) = delete;//根據超時時間新建定時器并插入時間輪中tw_timer* add_timer(int time_out){//如果超時時間為負數則直接返回if(time_out < 0){return nullptr;}int ticks = 0; //移動多少個槽時觸發//如果超時時間小于一個時間間隔,則槽數取整為1if(time_out < SLOT_INERTVAL){ticks = 1;}else{//計算移動的槽數ticks = time_out / SLOT_INERTVAL;}int rotation = ticks / SLOT_COUNT; //計算插入的定時器移動多少圈后會被觸發int time_slot = (cur_slot + (ticks % SLOT_COUNT) % SLOT_COUNT); //計算其應該插入的槽位tw_timer* timer = new tw_timer(rotation, time_slot);//如果要插入的槽為空,則成為該槽的頭節點if(_slots[time_slot] == nullptr){_slots[time_slot] = timer;}//否則頭插進入該槽中else{timer->_next = _slots[time_slot];_slots[time_slot]->_prev = timer;_slots[time_slot] = timer;}return timer;}//刪除指定定時器void del_timer(tw_timer* timer){if(timer == nullptr){return;}int time_slot = timer->_time_slot;//如果該定時器為槽的頭節點,則讓下一個節點成為新的頭節點if(timer == _slots[time_slot]){_slots[time_slot] = _slots[time_slot]->_next;if(_slots[time_slot]){_slots[time_slot]->_prev = nullptr;}delete timer;timer = nullptr;}//此時槽為中間節點,正常的鏈表刪除操作即可else{timer->_prev->_next = timer->_next;if(timer->_next){timer->_next->_prev = timer->_prev;}delete timer;timer = nullptr;} }//處理當前槽的定時事件,并使時間輪轉動一個槽void tick(){tw_timer* cur = _slots[cur_slot];while(cur){//如果不在本輪進行處理,則輪數減一后跳過if(cur->_rotation > 0){--cur->_rotation;cur = cur->_next;}//本輪需要處理的定時器,執行定時任務后將其刪除else{cur->fun(cur->_user_data);//如果刪除的是頭節點if(cur == _slots[cur_slot]){_slots[cur_slot] = cur->_next;if(_slots[cur_slot]){_slots[cur_slot]->_prev = nullptr;}delete cur;cur = _slots[cur_slot];}//刪除的是中間節點else{cur->_prev->_next = cur->_next;if(cur->_next){cur->_next->_prev = cur->_prev;} tw_timer* next = cur->_next;delete cur;cur = next;}}}//本槽處理完成,時間輪轉動一個槽位cur_slot = (cur_slot + 1) % SLOT_COUNT;}private:tw_timer* _slots[SLOT_COUNT]; //時間輪的槽,每個槽的元素為一個無序定時器鏈表int cur_slot; //當前指向的槽 };#endif // !__TIMER_WHEEL_H__ 時間復雜度
添加節點:O(1)
刪除節點:O(1)
執行定時任務:O(n)
雖然執行定時任務的時間復雜度為O(n),但是當我們使用多個輪子來實現時間輪時,時間復雜度會接近于O(1)
時間堆
在我們前面討論的定時器鏈表、時間輪都是以固定的時間間隔來調用到時檢測函數tick來檢測是否到期,然后執行到期定時器的回調函數,這樣的容器存在一個嚴重的缺點,就是定時不夠精確。
為了解決這個缺點,在設計定時器容器的時候可以采用另外一種思路,將所有定時器中超時時間最小的定時器的超時時間設置為時間間隔。一旦tick被調用超時時間最小的定時器必然到期,對其進行處理。接著我們再從剩余的定時器中找出超時時間最小的,繼續以上邏輯。
通過這種方法,就可以實現準確的定時。而這種數據結構,恰好和我們之前學過的堆相同,所以我們又將這種以最小堆實現的定時器容器稱為時間堆。
關于堆的基本操作在這里就不贅述了,如果不了解的可以參考我的往期博客
數據結構與算法 | 堆
時間復雜度
添加節點:O(logN)
刪除節點:O(logN)
執行定時任務:O(1)
總結
以上是生活随笔為你收集整理的Linux网络编程 | 高性能定时器 :时间轮、时间堆的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux网络编程 | 定时事件 :Li
- 下一篇: Linux网络编程 | 零拷贝 :sen