定時器概述

来源:https://www.cnblogs.com/zqurgy/archive/2023/05/14/17400165.html
-Advertisement-
Play Games

定時器詳解 引出 定時器是一個比較常見的數據結構,或者說框架,以一個最簡單的例子引出,在游戲中,冷卻時間使用的就是定時器; 所以說定時器是**等待時間過期執行對應時間事件處理( 回調函數 )**的一個框架; 補充:下文中可能會出現定時任務,它和時間事件基本上是一個東西 那麼現在有一個就有一個問題,該 ...


定時器詳解

引出

定時器是一個比較常見的數據結構,或者說框架,以一個最簡單的例子引出,在游戲中,冷卻時間使用的就是定時器;

所以說定時器是等待時間過期執行對應時間事件處理( 回調函數 )的一個框架;

補充:下文中可能會出現定時任務,它和時間事件基本上是一個東西

那麼現在有一個就有一個問題,該如何實現定時器這一功能?

  • 首先進行兩種分類:隨著網路事件處理定時事件;不隨著網路事件處理時間事件;

定時器概述

對於一個伺服器來說,需要許多客戶端進行通信,必然會有網路IO相關的事件( 網路IO事件 );

除此之外,伺服器內部對於一個或N個客戶端傳輸過來的數據進行延時相關的處理,針對不同送達時間,必然會有排序和時間事件

對於不同的框架,針對網路事件和時間會有不同的實現:

  • 第一種,網路事件和定時事件在同一個線程中使用;
  • 第二種:網路事件和定時事件在不同的線程中使用;

第一種形式-網路事件和定時事件在同一個線程中使用

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, 
           struct timeval *timeout);
timeout > 0:表示等待指定的時間,單位為毫秒;
timeout == 0:表示立即返回,不管是否有事件發生;
timeout == -1:表示一直等待,直到有事件發生。
  • 這一模式中,利用的是IO多路復用中的timeout參數;使用其大於0與等於0的情況,以一下代碼舉例。
// ... 其他代碼
while(!quit){
    
	int timeout=get_nearest_timer()-now_time();// get_nearest_timer為得到最近的時間事件
	if(timeout<0)
		timeout=-1;
	int event_count=epoll_wait(epfd,ev,ev_num,timeout);
	for(int i=0;i<event_count;i++){
        
		// ... 處理網路事件
	}
    
	update_timer();// 處理時間事件
	// ... 其他代碼
}
// ... 其他代碼

說明:get_nearest_timer()、update_timer(),為定時器提供

  • 從上述中可知其流程:
    • 最近定時任務需要的時間->epoll_wait阻塞一會兒->處理網路事件->處理時間事件;
  • update_timer()函數可以只是用作確定時間事件的回調函數,可以把事件處理拋給線程池,而update_timer()會直接進行返回,不會阻塞主流程,這是一種事件的非同步形式。

總結

針對於上述流程,最關鍵的一步就是使用epoll_wait進行阻塞,使得可以同步地進行網路IO處理和定時任務處理,讓其處於一個流程中

當然這種必然也是有缺點的,比如定時任務或者網路IO數量太多,導致處理時間太長,不過這可以使用 reactor模型( 或者使用協程)+時間事件非同步處理 的形式解決。多數情況下還是針對少量定時任務使用第一種。

再有就是多線程環境下,如果網路IO的事件處理和時間任務處理需要有一種同步的關係,大概還得進行加鎖的複雜處理

擴展

  • 其實reactor模型也是非同步事件處理,從而讓IO處理時同步的

第二種:網路事件和定時事件在不同的線程中使用

概況:時間事件使用一個單獨的線程進行檢測,一旦檢測到進行回調、或者再拋給任務隊列,由線程池進行處理;

void* thread_timer(void * thread_param) {
    init_timer();// 初始化定時器
    while (!quit) {
        update_timer(); // 檢測定時器中的定時任務
        sleep(t); // 這⾥的 t 要⼩於 時間精度
    }
    clear_timer();
    return NULL;
}
pthread_create(&pid, NULL, thread_timer, &thread_param);

說明:

  • 定時器內部會維護大量的定時任務,update_timer()是用於檢測定時器中所有需要被處理的定時任務,檢測到後進行處理並刪除該定時任務,處理時可以直接調用回調,也可以做成非同步的事件處理。

  • 使用sleep()/usleep()函數,減少進入update_timer()函數的次數,不過一定要讓t小於時間精度(最小運行單元),否則t大於時間精度會讓定時器不會及時的被檢測到,會出現延時的狀況。

總結

在不同線程處理網路事件和時間事件時,可以進行大量的定時任務的維護和處理,讓網路IO處理和定時任務解耦。

定時器設計

既然要使用定時器,必然離不開定時器的設計,那麼如何設計定時器,成為接下來需要解決的問題。

介面設計

首先是介面的設計

基本介面:

  1. 初始化定時器:void init_timer();
  2. 添加定時任務:Node *add_timer(int expire,callback cb);
  3. 刪除定時任務:bool delete_timer(Node* node);一般在檢測定時器的時候使用,可以不對外提供該介面;
  4. 找到最近的需要被觸發的定時任務:Node* find_nearest_timer();這種介面一般應用於網路事件和定時事件在一個線程中處理的觸發方式。
  5. 檢測更新所有定時任務:void update_timer();
  6. 清除所有定時任務 :void clear_timer();

內部數據結構選擇

對外提供的介面解決了,那麼接下來該思考定時器內部該用什麼樣的數據結構維護定時任務會讓效率達到最佳;

該數據結構需要滿足一些條件,比如查詢最小節點的時間複雜度比如得小,比如增加刪除節點不會影響原有的數據結構。

因此選擇出來了,紅黑樹、最小堆都是最佳的選擇。

紅黑樹

紅黑樹的中序遍歷,讓數據可以有序排序,而且是絕對排序(從小到大排序)。具體數據結構,可以去查找網上其他資料。

  • 增刪查的時間複雜度均為O(logN);
  • 最小的節點為紅黑樹最左側的節點,時間複雜度O(logN);
  • 如果幾乎同時來個兩個時間相等的任務,但是key值比較時會出現相等的情況,你可以把後來的任務放到其右節點位置。

最小堆

最小堆是一個完全二叉樹,且父節點一定比子節點小,這樣得到的結果就是根節點一定是這棵樹的最小值;具體數據結構,可以去查找網上其他資料。

  • 增查的時間複雜度O(logN);

  • 刪除操作需要查找是否包含這個節點,刪除的時間複雜度O(n);

  • 最小節點的時間複雜度為O(1);

時間輪

以上兩種實現方法需要時時刻刻都要進行檢測,對於定時任務密集類型的自然可以,如果是定時任務之間的間隔時間過長怎麼辦?時時刻刻檢測會增加很多無效檢測,降低cpu的使用效率。

使用時間輪是一種解決上述問題的方法;

時間輪需要保證自己的時間精度足夠小,否則會出現不能插入定時任務的情況

單層級時間輪

首先先談一談單層級時間輪,單層級時間輪就是使用數組模擬一個環形隊列,數組的每一個元素是一個定時任務鏈表。通過一個指針不斷遍歷這個數組,遇到某個元素的任務鏈表不為空就執行相應的任務。

對於單層級時間輪需要數組的大小足夠大,否則插入的定時任務超出數組可關註時間(比如時間精度1ms,數組大小為20,可關註的時間就是20ms)的N多倍,會導致定時任務執行順序出錯。(不確定在定時任務的結構體增加一個圈數可不可以);

多層級時間輪

那麼多層級時間輪就是解決上述問題一個好的解決辦法。類似於進位進位一樣,一旦超過該層級的可關註時間,就會進入下一層級,直到有位置可以插入;如果某個定時任務即將執行,也會不斷返回上一層級,直至掛載到第一層級(也是被執行層級)。

如果使用時間輪也是直接使用多層級時間輪的,它有以下這些優點:

  • 相比於單層級時間輪,它的空間占用大大減少;因為多層級針對任務的時間儲存是乘的關係,而單層級是加的關係
  • 對於多層級,除去第一層的任務需要時時刻刻關註,其他層的元素每增加一次重新映射一次即可。

數據結構

struct timer_event {
	uint32_t handle;
	int session;
};
// 定時任務
struct timer_node {
	struct timer_node *next;
	uint32_t expire;
};
// 任務鏈表
struct link_list {
	struct timer_node head;
	struct timer_node *tail;
};
struct timer {
	struct link_list near[TIME_NEAR];
	struct link_list t[4][TIME_LEVEL];
	struct spinlock lock;	// 自旋鎖,多線程操作需要,時間複雜度O(1)
	uint32_t time;			//時間指針,範圍是2^32*10ms
	uint32_t starttime;
	uint64_t current;
	uint64_t current_point;
};

因此針對時間輪設計,需要確定幾點:

  1. 確定時間範圍。每一層級允許的添加任務的時間範圍
  2. 確定時間精度。每一次tick(指針增加一次)的時間
  3. 確定時間層級。第一層組織最近關註的延時任務,是實際執行的層級;其他層級只負責向上一層級重新映射。
  4. 實現添加任務介面。
  5. 實現刪除任務的介面。對於刪除任務,一般不採取直接從鏈表刪除的方式,因為時間指針一直再增加,定時任務可能會被重新映射,節點位置發生改變。因此,可以通過添加一個標記欄位cancel,當cancel=true時不執行具體任務。
  6. 實現重新映射,每一次時間指針移動都需要判斷是否可重新映射。
時間輪總結

時間輪顧名思義,是仿照時鐘規律而發明出來的。使用時間輪就是使用多層級時間輪。

時間輪通過多個層級存放定時任務,第一層組織最近關註的延時任務,是實際執行的層級;其他層級只負責向上一層級重新映射。

對於相同時間觸發的定時任務,時間輪採用一個鏈表將它們存放在同一個時間槽,時間到達時一起執行。

多線程下,定時器只管檢測,檢測到了定時任務,將其拋給線程池執行,定時器本身線程不執行定時任務的業務邏輯。

時間輪刪除節點很不方便,一般不採用刪除方式,因為時間指針一直在移動,定時任務可能會被重新映射,節點位置發生改變;因此,可以通過添加一個標記欄位cancel,當cancel=true時不執行具體任務。

內部數據結構選擇總結

多線程環境下,使用最小堆和紅黑樹的定時器,在進行添加任務、刪除任務的時候需要把整個樹進行加鎖,使用互斥鎖;如果是時間輪,由於其時間複雜度為O(1),對於添加任務、刪除任務使用自旋鎖即可。

如果定時任務比較少,可以選擇紅黑樹或者最小堆;如果定時任務比較多,選擇時間輪。

說明

寫這篇文章的目的,是捋清定時器會有哪些問題,從巨集觀的一個角度去看待定時器,如果內容有哪些地方不夠細節,可以利用谷歌、必應進行搜索,深度瞭解其原理、或者實現代碼。比如紅黑樹的數據結構是什麼樣的、最小堆的數據結構是什麼樣的等這些問題我相信有很多大牛會比我講的更好,或者說有著更深刻的理解。

此外,本人目前只是一名大學生,對於編程自認為沒有達到很高的水平,而且很多地方也有一些借鑒的成分,如果文章中有任何不對的地方,歡迎指正,本人會虛心接受的。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本篇將講講登錄庫中的三種登錄模式的實現: 同一用戶只能登錄一次, 同一用戶多次登錄多token,同一用戶多次登錄共用一個token,源碼:weloe/token-go: a light login library (github.com) ...
  • 一 進程對象及其他方法 '''一臺電腦上面運行著很多進程,那麼電腦是如何區分並管理這些進程服務端的呢?電腦會給每一個運行的進程分配一個PID號如何查看 windows電腦 進入cmd輸入tasklist即可查看 tasklist|findstr PID查看具體的進程 linux電腦 進入終端之 ...
  • ABP框架 ABP是用於創建現代化Web應用程式的完整體繫結構和強大的基礎架構,以模塊化的方式進行開發,所有模塊以nuget包的方式提供,開箱即用,遵循最佳實踐和約定,提供SOLID開發經驗。 | 縮寫 | 英文 | 中文 | |--|--|--| | SRP | The Single Respon ...
  • 一、概述 PUT 和 PATCH 方法用於更新現有資源。 它們之間的區別是,PUT 會替換整個資源,而 PATCH 僅指定更改。 在 ASP.NET Core Web API 中,由於 C# 是一種靜態語言(dynamic 在此不表),當我們定義了一個類型用於接收 HTTP Patch 請求參數的時 ...
  • 前言 之前有個項目需要執行一個略微耗時的操作大概五六七八九十秒這樣子,這個時候程式不能做其他操作,只能等待操作完成。為了提升一絲使用體驗同時讓Winform程式看上去高級一點🎃🎃🎃,就想到加一個遮罩層(MaskLayer)。雖然Winform沒有原生的遮罩層,但是實現起來也不是很麻煩。 遮罩層 ...
  • 目錄 沁恆 CH32V208(一): CH32V208WBU6 評估板上手報告和Win10環境配置 沁恆 CH32V208(二): CH32V208的儲存結構, 啟動模式和時鐘 沁恆 CH32V208(三): CH32V208 Ubuntu22.04 Makefile VSCode環境配置 沁恆 C ...
  • 以 redhat7.4為例,網上的解決方案多是針對ubuntu的,需要進入ubuntu的預覽系統,redhat好像沒這個東西 問題:新添磁碟後開機無法進入系統。 似乎是因為電腦將新增的空硬碟作為了系統盤進行啟動,所以無法啟動系統。 解決方案:只要讓電腦將裝有linux系統的硬碟進行啟動就可以解決 ...
  • 切換 Windows 的系統語言 Windows 10 專業版 (1)點擊左下角開始菜單欄 --> 設置 --> 時間和語言 --> 語言。 (2)點擊添加語言,在彈出的列表框中,選擇你要安裝的語言。 (3)下載完語言包後,點擊 Windows 顯示語言下拉框,選擇剛剛安裝的語言。 (4)選擇新的語 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...