這一篇文章系統的梳理主流定時器算法實現的差別以及應用地方。算法
程序裏的定時器主要實現的功能是在將來的某個時間點執行相應的邏輯。在定時器模型中,通常有以下幾個定義。 數組
interval:間隔時間,即定時器須要在interval時間後執行數據結構
StartTimer:添加一個定時器任務ide
StopTimer:結束一個定時器任務函數
PerTickBookkeeping: 檢查定時器系統中,是否有定時器實例已經到期,至關於定義了最小時間粒度。性能
常見的實現方法有以下幾種:優化
鏈表線程
排序鏈表3d
最小堆指針
時間輪
接下來咱們一塊兒看下這些方法的具體實現原理。
2.1 鏈表實現
鏈表的實現方法比較粗糙。鏈表用於存儲全部的定時器,每一個定時器都含有interval 和 elapse 兩個時間參數,elapse表示當前被tickTimer了多少次。當elapse 和interval相等時,表示定時器到期。
在此方案中,添加定時器就是在鏈表的末尾新增一個節點,時間複雜度是 O(1)。若是想要刪除一個定時器的話,咱們須要遍歷鏈表找到對應的定時器,時間複雜度是O(n)。此方案下,每隔elapse時間,系統調用信號進行超時檢查,即PerTickBookkeeping。每次PerTickBookkeeping須要對鏈表全部定時器進行 elapse++,所以能夠看出PerTickBookkeeping的時間複雜度是O(N)。能夠看出此方案過於粗暴,因此使用場景極少
2.2 排序雙向鏈表實現
排序雙向鏈表是在鏈表實現上的優化。優化思路是下降時間複雜度。
首先,每次PerTickBookkeeping須要自增全部定時器的elapse變量,若是咱們將interval變爲絕對時間,那麼咱們只須要比較當前時間和interval時間是否相等,減小了對每一個定時器的操做。若是不須要對每一個定時器進行操做,咱們將定時器進行排序,那麼每次PerTickBookkeeping都只須要判斷第一個定時器,時間複雜度爲O(1)。相應的,爲了維持鏈表順序,每次新增定時器須要進行鏈表排序時間複雜度爲 O(N)。每次刪除定時器時,因爲會持有本身節點的引用,因此不須要查找其在鏈表中所在的位置,因此時間複雜度爲O(1),雙向鏈表的好處。
圖1 雙向鏈表實現示意圖
2.3 時間輪實現
時間輪示意圖以下:
圖2 時間輪
時間輪的數據結構是數組 + 鏈表。 他的時間輪爲數組,新增和刪除一個任務,時間複雜度都是O(1)。PerTickBookkeeping每次轉動一格,時間複雜度也是O(1)。
2.4 最小堆實現
最小堆是堆的一種, (堆是一種二叉樹), 指的是堆中任何一個父節點都小於子節點, 子節點順序不做要求。
二叉排序樹(BST)指的是: 左子樹節點小於父節點, 右子樹節點大於父節點, 對全部節點適用
圖3 最小堆
樹的基本操做是插入節點和刪除節點。對最小堆而言,爲了將一個元素X插入最小堆,咱們能夠在樹的下一個空閒位置建立一個空穴。若是X能夠放在空穴中而不被破壞堆的序,則插入完成。不然就執行上濾操做,即交換空穴和它的父節點上的元素。不斷執行上述過程,直到X能夠被放入空穴,則插入操做完成。所以咱們能夠知道最小堆的插入時間複雜度是O(lgN)。最小堆的刪除和插入邏輯基本相似,若是不作優化,時間複雜度也是O(lgN),可是實際實現方案上,作了延遲刪除操做,時間複雜度爲O(1)。
延遲刪除即設置定時器的執行回調函數爲空,每次最小堆超時,將觸發pop_heap,pop會從新調整最小堆,最終刪除的定時器將調整到堆頂,可是回調函數不處理。
能夠看到PerTickBookkeeping只處理堆頂定時器,時間複雜度O(1)。最小堆可使用數組來進行表示,數組中,當前下標n的左子節點爲2N + 1,當前下標n的右子節點小標爲2N + 2。
圖4 最小堆的數組表示
3.1 時間複雜度對比
圖5 不一樣實現時間複雜度
從上面的介紹來看,時間輪的時間複雜度最小、性能最好。
3.2 使用場景來看
在任務量小的場景下:最小堆實現,能夠根據堆頂設置超時時間,數組存儲結構,節省內存消耗,使用最小堆能夠獲得比較好的效果。而時間輪定時器,因爲須要維護一個線程用來撥動指針,且須要開闢一個bucket數組,消耗內存大,使用時間輪會較爲浪費資源。在任務量大的場景下:最小堆的插入複雜度是O(lgN), 相比時間輪O(1) 會形成性能降低。更適合使用時間輪實現。在業界,服務治理的心跳檢測等功能須要維護大量的連接心跳,所以時間輪是首選。
更多免費技術資料及視頻