.netframework如何實現(xiàn)高精度定時器

這篇文章主要介紹了.net framework如何實現(xiàn)高精度定時器,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

站在用戶的角度思考問題,與客戶深入溝通,找到鐵西網(wǎng)站設(shè)計與鐵西網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗,讓設(shè)計與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個性化、用戶體驗好的作品,建站類型包括:網(wǎng)站建設(shè)、網(wǎng)站制作、企業(yè)官網(wǎng)、英文網(wǎng)站、手機端網(wǎng)站、網(wǎng)站推廣、主機域名、雅安服務(wù)器托管、企業(yè)郵箱。業(yè)務(wù)覆蓋鐵西地區(qū)。

1背景

.NET Framework 提供了四種定時器,然而其精度都不高(一般情況下 15ms 左右),難以滿足一些場景下的需求。

在進行媒體播放、繪制動畫、性能分析以及和硬件交互時,可能需要 10ms 以下精度的定時器。這里不討論這種需求是否合理,它是確實存在的問題,也有相當多的地方在討論,說明這是一個切實的需求。然而,實現(xiàn)它并不是一件輕松的事情。

這里并不涉及內(nèi)核驅(qū)動層面的定時器,只分析在 .NET 托管環(huán)境下應(yīng)用層面的高精度定時器實現(xiàn)。

Windows 不是實時操作系統(tǒng),所以任何方案都無法絕對保證定時器的精度,只是能盡量減少誤差。所以,系統(tǒng)的穩(wěn)定性不能完全依賴于定時器,必須考慮失去同步時的處理。

2等待策略

想要實現(xiàn)高精度定時器,必然需要等待和計時兩種基礎(chǔ)功能。等待用來跳過一定時間間隔,計時可以進行時間檢查,用以調(diào)整等待時間。

等待策略實際就是兩種:

  • 自旋等待:讓 CPU 空轉(zhuǎn)消耗時間,占用大量 CPU 時間,但是時間高度可控。

  • 阻塞等待:線程進入阻塞狀態(tài),出讓 CPU 時間片,在等待一定時間后再由操作系統(tǒng)調(diào)度回到運行狀態(tài)。阻塞時不占用 CPU,然而需要操作系統(tǒng)調(diào)度,時間難以控制。

可以看到二者各有優(yōu)劣,應(yīng)該按照不同需求進行不同的實現(xiàn)。

而計時機制可以說能用的只有一種,就是Stopwatch類。它內(nèi)部使用了系統(tǒng) API QueryPerformanceCounter/ QueryPerformanceFrequency來進行高精度計時,依賴于硬件,它的精度可以高達幾十納秒,非常適合用來實現(xiàn)高精度定時器。

所以難點在于等待策略,下面先分析簡單的自旋等待。

2.1自旋等待

可以使用Thread.SpinWait(int iteration)來進行自旋,也就是讓 CPU 在一個循環(huán)里空轉(zhuǎn),iteration參數(shù)是迭代次數(shù)。.NET Framework 中不少同步構(gòu)造都用到了它,用來等待一小段時間,減少上下文切換的開銷。

這里很難根據(jù)iteration來計算消耗的時間,因為 CPU 速度可能是動態(tài)的。所以需要結(jié)合使用Stopwatch。偽代碼如下:

var 等待開始時間 = 當前計時;while ((當前計時 - 等待開始時間) < 需要等待的時間){自旋;}

寫成實際代碼:

void Spin(Stopwatch w, int duration){var current = w.ElapsedMilliseconds;while ((w.ElapsedMilliseconds - current) < duration)Thread.SpinWait(10);}

這里的w是一個已經(jīng)啟動的Stopwatch,為了演示簡單使用了ElapsedMilliseconds屬性,精度是毫秒級的,使用ElapsedTicks屬性就可以獲得更高的精度(微秒級)。

然而如前所述,這樣精度高但是是以消耗 CPU 時間為代價的,這樣實現(xiàn)定時器會讓一個 CPU 核心滿負荷工作(如果執(zhí)行的任務(wù)也沒有阻塞的話)。相當于浪費了一個核心,在有些時候不太現(xiàn)實(比如核心很少甚至是單核的虛擬機上),所以需要考慮阻塞等待。

2.2阻塞等待

阻塞等待會把控制權(quán)交給操作系統(tǒng),這樣就必須確保操作系統(tǒng)能夠及時的將定時器線程調(diào)度回運行狀態(tài)。默認情況下,Windows 的系統(tǒng)定時器精度是 15.625ms,也就是說時間切片是這個尺寸。如果線程阻塞,出讓其時間片進行等待,再被調(diào)度運行的時間至少是一個切片 15.625ms。那么必須減少時間切片的長度,才有可能實現(xiàn)更高的精度。

可以通過系統(tǒng) API timeBeginPeriod來修改系統(tǒng)定時器精度到 1ms(它內(nèi)部使用了沒有給出文檔的NtSetTimerResolution,這個 API 可以修改到 0.5ms)。不需要的時候使用timeEndPeriod還原。

修改系統(tǒng)定時器精度有副作用。它會增加上下文切換的開銷,增加耗電量,降低系統(tǒng)整體性能。然而,很多程序都不得不這么做,因為沒有其它方式能獲得更高的定時器精度。比如基于 WPF 的程序(包括 Visual Studio)、使用 Chromium 內(nèi)核的應(yīng)用(Chrome、QQ)、多媒體播放器、游戲等等很多程序都會在一定時間內(nèi)把系統(tǒng)定時器精度修改到 1ms。(查看方法見后面)

所以實際上這個副作用在桌面環(huán)境已經(jīng)成為常態(tài)。而且從 Windows 8 開始,這個副作用減弱了。

在 1ms 的系統(tǒng)定時器精度前提下,可以使用三種方式實現(xiàn)阻塞等待:

  • Thread.Sleep

  • WaitHandle.WaitOne

  • Socket.Poll

另外,多媒體定時器timeSetEvent也使用了阻塞的方式。

Thread.Sleep

它的參數(shù)使用毫秒單位,所以最多只能精確到 1ms。不過事實上很不穩(wěn)定,Thread.Sleep(1)會在 1ms 與 2ms 兩種狀態(tài)間跳動,也就是可能會產(chǎn)生 +1ms 多的誤差。

實測發(fā)現(xiàn),沒有任務(wù)負載的情況下(純粹循環(huán)調(diào)用Sleep(1)),阻塞時長穩(wěn)定在 2ms;而有任務(wù)負載時,則至少會阻塞 1ms。這和其它兩種阻塞方式不同,詳見后文。

如果需要修正這個誤差,可以在阻塞 n 毫秒時,使用Sleep(n-1),并通過Stopwatch計時,剩余等待時間用Sleep(0)Thread.Yield或自旋來補充。

Sleep(0)會出讓剩余的 CPU 時間片給優(yōu)先級相同的線程,而Thread.Yield是出讓剩余的 CPU 時間片給運行在同一核心上的線程。在出讓的時間片結(jié)束后,其會被重新調(diào)度。一般情況下,整個過程可以在 1ms 之內(nèi)完成。

Thread.Sleep(0)Thread.Yield在 CPU 高負載情況下非常不穩(wěn)定,實測可能會阻塞高達 6ms 時間,所以可能會產(chǎn)生更多的誤差。因此誤差修正最好通過自旋方式實現(xiàn)。

WaitHandle.WaitOne

WaitHandle.WaitOneThread.Sleep類似,參數(shù)也是毫秒單位。

不同之處是,沒有任務(wù)負載的情況下(純粹循環(huán)調(diào)用WaitOne(1)),阻塞時長穩(wěn)定在 1.5ms;而有任務(wù)負載時,則可能僅阻塞近乎于 0 的時間(猜測是它僅阻塞到當前時間片結(jié)束,尚未找到具體的文檔說明)。所以它阻塞的時長范圍是 0 到 2ms 多。

WaitHandle.WaitOne(0)是用來測試等待句柄狀態(tài)的,它并不阻塞,所以用它來進行誤差修正類似于自旋,但不如直接使用自旋可靠。

Socket.Poll

Socket.Poll方法的參數(shù)是以微秒為單位,理論上,它是使用了網(wǎng)卡的硬件來定時,精度很高。然而,由于阻塞的實現(xiàn)仍然要依賴線程,所以它也只能達到 1ms 的精度。

它的優(yōu)勢是比Thread.SleepWaitHandle.WaitOne要更穩(wěn)定,誤差也更小,可以不需要修正,但要占用一個 Socket 端口。

沒有任務(wù)負載的情況下(純粹循環(huán)調(diào)用Poll(1)),阻塞時長穩(wěn)定在 1ms;而有任務(wù)負載時,則和WaitOne類似,可能僅阻塞近乎于 0 的時間。所以它阻塞的時長范圍是 0 到 1ms 多。

Socket.Poll(0)是用來測試 Socket 狀態(tài)的,但它會阻塞,而且可能阻塞高達 6ms,所以不能用它來進行誤差修正。

timeSetEvent

timeSetEvent和之前提到的timeBeginPeriod一樣屬于 winmm.dll 提供的多媒體定時器功能。它可以直接當作定時器使用,也是提供 1ms 的精度。在不需要的時候使用timeKillEvent來關(guān)閉。

它的穩(wěn)定性和精度也很高,如果需要 1ms 的定時,而又不能使用自旋,那么這是最理想的方案。

雖然 MSDN 上說timeSetEvent是個過時的方法,應(yīng)該用CreateTimerQueueTimer替換。但是CreateTimerQueueTimer精度和穩(wěn)定性都不如多媒體定時器,所以在需要高精度的時候,只能使用timeSetEvent

3定時器實現(xiàn)

需要注意的是,無論自旋還是阻塞,顯然定時器都應(yīng)該運行在獨立的線程,不能干擾使用方線程工作。而對于高精度定時器來說,觸發(fā)事件以執(zhí)行任務(wù)的線程一般都在定時器線程內(nèi),而不是再使用獨立的任務(wù)線程。

這是因為高精度定時場景下,執(zhí)行任務(wù)的時間開銷很可能大于定時器的時間間隔,如果默認就在其它線程執(zhí)行任務(wù),可能導致占用大量線程。所以應(yīng)該把控制權(quán)交給用戶,讓用戶在需要的時候自行調(diào)度任務(wù)執(zhí)行的線程。

3.1觸發(fā)模式

由于在定時器線程執(zhí)行任務(wù),所以定時器的觸發(fā)就產(chǎn)生了三種模式。以下是它們的說明和主循環(huán)偽代碼:

  • 固定時間框架

  • 比如間隔 10ms,任務(wù) 7-12ms,則會按照等待 10ms 、任務(wù) 7ms、等待 3ms、任務(wù) 12ms(超時 2ms 失去同步)、任務(wù) 7ms、等待 1ms(回到同步)、任務(wù) 7ms、等待 3ms、… 進行。就是盡量按照設(shè)定好的時間框架來執(zhí)行任務(wù),只要任務(wù)不是始終超時,就可以回到原本的時間框架上。

var 下一幀時間 = 0;while(定時器開啟){下一幀時間 += 間隔時間;while (當前計時 < 下一幀時間){等待;}觸發(fā)任務(wù);}
  • 可推遲時間框架

  • 上面的例子會按照等待 10ms 、任務(wù) 7ms、等待 3ms、任務(wù) 12ms(超時,推遲時間框架 2ms)、任務(wù) 7ms、等待 3ms、… 進行。超時的任務(wù)會推遲時間框架。

var 下一幀時間 = 0;while(定時器開啟){下一幀時間 += 間隔時間;if (下一幀時間 < 當前計時)下一幀時間 = 當前計時while (當前計時 < 下一幀時間){等待;}觸發(fā)任務(wù);}
  • 固定等待時間

  • 上面的例子會按照等待 10ms、任務(wù) 7ms、等待 10ms、任務(wù) 12ms、等待 10ms、任務(wù) 7ms… 進行。等待時間始終不變。

while(定時器開啟){var 等待開始時間 = 當前計時;while ((當前計時 - 等待開始時間) < 間隔時間){等待;}觸發(fā)任務(wù);}// 或者:var 下一幀時間 = 0;while(定時器開啟){下一幀時間 += 間隔時間;while (當前計時 < 下一幀時間){等待;}觸發(fā)任務(wù);下一幀時間 = 當前計時;}

如果使用多媒體定時器(timeSetEvent),它固定實現(xiàn)了第一種模式,而其它的等待策略能夠?qū)崿F(xiàn)全部三種模式,可以根據(jù)需求選擇。

while循環(huán)中的等待可以使用自旋或阻塞,也可以結(jié)合它們來達到精度、穩(wěn)定性和 CPU 開銷的平衡。

另外,由上面的偽代碼可以看出,這三種模式的實現(xiàn)可以統(tǒng)一,能夠做到根據(jù)情況切換。

3.2線程優(yōu)先級

最好把線程優(yōu)先級調(diào)高,以保證定時器能夠穩(wěn)定工作,減少被搶占的機會。然而需要注意,這在 CPU 資源不足時可能導致低優(yōu)先級線程的饑餓。也就是說不能讓高優(yōu)先級線程去等待低優(yōu)先級線程改變狀態(tài),很有可能低優(yōu)先級線程沒有機會運行,導致死鎖或類似死鎖的狀態(tài)。(見一種類似的饑餓的例子)

線程的最終優(yōu)先級和進程的優(yōu)先級有關(guān),所以有時候也需要提高進程優(yōu)先級(見 C# 中的多線程系列的線程優(yōu)先級說明)。

4其它

還有兩點需要注意:

  1. 線程安全:定時器在獨立線程運行,其暴露的成員都應(yīng)該實現(xiàn)線程安全,否則在定時器運行時調(diào)用可能會產(chǎn)生問題。

  2. 及時釋放資源:多媒體定時器、等待句柄、線程等等這些都是系統(tǒng)資源,在不需要它們的時候應(yīng)該及時釋放/銷毀。

如何查看系統(tǒng)定時器精度?

簡單的查看可以使用Sysinternals工具包中的 ClockRes,它會顯示如下信息:

Maximum timer interval: 15.625 ms
Minimum timer interval: 0.500 ms
Current timer interval: 15.625 ms

// 或

Maximum timer interval: 15.625 ms
Minimum timer interval: 0.500 ms
Current timer interval: 1.000 ms

如果是想查看哪些程序請求了更高的系統(tǒng)定時器精度,那么運行:

powercfg energy -duration 5

它會監(jiān)視系統(tǒng)能耗 5s,然后在當前目錄生成一個energy-report.html的分析報告,可以打開它查看。

找到里面的警告部分,會有平臺計時器分辨率:未完成的計時器請求Platform Timer Resolution:Outstanding Timer Request)信息。

感謝你能夠認真閱讀完這篇文章,希望小編分享的“.net framework如何實現(xiàn)高精度定時器”這篇文章對大家有幫助,同時也希望大家多多支持創(chuàng)新互聯(lián),關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,更多相關(guān)知識等著你來學習!

分享名稱:.netframework如何實現(xiàn)高精度定時器
文章分享:http://muchs.cn/article36/iepppg.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站制作商城網(wǎng)站、網(wǎng)頁設(shè)計公司、電子商務(wù)網(wǎng)站收錄、建站公司

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

網(wǎng)站優(yōu)化排名