C#中自定義高精度Timer定時(shí)器的實(shí)例教程
1、背景
在C#里關(guān)于定時(shí)器的類(lèi)就有3個(gè):
(1)定義在System.Windows.Forms里
(2)定義在System.Threading.Timer類(lèi)里
(3)定義在System.Timers.Timer類(lèi)里
Timer 用于以用戶(hù)定義的事件間隔觸發(fā)事件。Windows 計(jì)時(shí)器是為單線程環(huán)境設(shè)計(jì)的,其中,UI 線程用于執(zhí)行處理。它要求用戶(hù)代碼有一個(gè)可用的 UI 消息泵,而且總是在同一個(gè)線程中操作,或者將調(diào)用封送到另一個(gè)線程。
使用此計(jì)時(shí)器時(shí),請(qǐng)使用控件的Tick事件執(zhí)行輪詢(xún)操作,或在指定的時(shí)間內(nèi)顯示啟動(dòng)畫(huà)面。每當(dāng) Enabled 屬性設(shè)置為true且Interval屬性大于0時(shí),將引發(fā)Tick事件,引發(fā)的時(shí)間間隔基于Interval屬性設(shè)置。
System.Windows.Forms.Timer是應(yīng)用于WinForm中的,他是通過(guò)Windows消息機(jī)制實(shí)現(xiàn)的,類(lèi)似于VB或Delphi中的Timer控件,內(nèi)部使用API SetTimer實(shí)現(xiàn)的。他的主要缺點(diǎn)是計(jì)時(shí)不精確,而且必須有消息循環(huán),Console Application(控制臺(tái)應(yīng)用程式)無(wú)法使用。
System.Timers.Timer和System.Threading.Timer很類(lèi)似,他們是通過(guò).NET Thread Pool實(shí)現(xiàn)的,輕量,對(duì)應(yīng)用程式、消息沒(méi)有特別的需要。System.Timers.Timer還能夠應(yīng)用于WinForm,完全取代上面的Timer控件。
然而其精度都不高(一般情況下 15ms 左右),難以滿(mǎn)足一些場(chǎng)景下的需求。
在進(jìn)行媒體 、繪制動(dòng)畫(huà)、性能分析以及和硬件交互時(shí),可能需要 10ms 以下精度的定時(shí)器。這里不討論這種需求是否合理,它是確實(shí)存在的問(wèn)題,也有相當(dāng)多的地方在討論,說(shuō)明這是一個(gè)切實(shí)的需求。然而,實(shí)現(xiàn)它并不是一件輕松的事情。
這里并不涉及內(nèi)核驅(qū)動(dòng)層面的定時(shí)器,只分析在 .NET 托管環(huán)境下應(yīng)用層面的高精度定時(shí)器實(shí)現(xiàn)。
Windows 不是實(shí)時(shí)操作系統(tǒng),所以任何方案都無(wú)法絕對(duì)保證定時(shí)器的精度,只是能盡量減少誤差。所以,系統(tǒng)的穩(wěn)定性不能完全依賴(lài)于定時(shí)器,必須考慮失去同步時(shí)的處理。
2、等待策略
想要實(shí)現(xiàn)高精度定時(shí)器,必然需要等待和計(jì)時(shí)兩種基礎(chǔ)功能。等待用來(lái)跳過(guò)一定時(shí)間間隔,計(jì)時(shí)可以進(jìn)行時(shí)間檢查,用以調(diào)整等待時(shí)間。
等待策略實(shí)際就是兩種:
自旋等待:讓 CPU 空轉(zhuǎn)消耗時(shí)間,占用大量 CPU 時(shí)間,但是時(shí)間高度可控。
阻塞等待:線程進(jìn)入阻塞狀態(tài),出讓 CPU 時(shí)間片,在等待一定時(shí)間后再由操作系統(tǒng)調(diào)度回到運(yùn)行狀態(tài)。阻塞時(shí)不占用 CPU,然而需要操作系統(tǒng)調(diào)度,時(shí)間難以控制。
可以看到二者各有優(yōu)劣,應(yīng)該按照不同需求進(jìn)行不同的實(shí)現(xiàn)。
而計(jì)時(shí)機(jī)制可以說(shuō)能用的只有一種,就是Stopwatch類(lèi)。它內(nèi)部使用了系統(tǒng) API QueryPerformanceCounter/ QueryPerformanceFrequency來(lái)進(jìn)行高精度計(jì)時(shí),依賴(lài)于硬件,它的精度可以高達(dá)幾十納秒,非常適合用來(lái)實(shí)現(xiàn)高精度定時(shí)器。
所以難點(diǎn)在于等待策略,下面先分析簡(jiǎn)單的自旋等待。
2.1自旋等待
可以使用Thread.SpinWait(int iteration)來(lái)進(jìn)行自旋,也就是讓 CPU 在一個(gè)循環(huán)里空轉(zhuǎn),iteration參數(shù)是迭代次數(shù)。.NET Framework 中不少同步構(gòu)造都用到了它,用來(lái)等待一小段時(shí)間,減少上下文切換的開(kāi)銷(xiāo)。
這里很難根據(jù)iteration來(lái)計(jì)算消耗的時(shí)間,因?yàn)?CPU 速度可能是動(dòng)態(tài)的。所以需要結(jié)合使用Stopwatch。偽代碼如下:
var 等待開(kāi)始時(shí)間 = 當(dāng)前計(jì)時(shí); while ((當(dāng)前計(jì)時(shí) - 等待開(kāi)始時(shí)間) < 需要等待的時(shí)間) { 自旋; }
寫(xiě)成實(shí)際代碼:
void Spin(Stopwatch w, int duration) { var current = w.ElapsedMilliseconds; while ((w.ElapsedMilliseconds - current) < duration) Thread.SpinWait(10); }
這里的w是一個(gè)已經(jīng)啟動(dòng)的Stopwatch,為了演示簡(jiǎn)單使用了ElapsedMilliseconds屬性,精度是毫秒級(jí)的,使用ElapsedTicks屬性就可以獲得更高的精度(微秒級(jí))。
然而如前所述,這樣精度高但是是以消耗 CPU 時(shí)間為代價(jià)的,這樣實(shí)現(xiàn)定時(shí)器會(huì)讓一個(gè) CPU 核心滿(mǎn)負(fù)荷工作(如果執(zhí)行的任務(wù)也沒(méi)有阻塞的話)。相當(dāng)于浪費(fèi)了一個(gè)核心,在有些時(shí)候不太現(xiàn)實(shí)(比如核心很少甚至是單核的虛擬機(jī)上),所以需要考慮阻塞等待。
2.2阻塞等待
阻塞等待會(huì)把控制權(quán)交給操作系統(tǒng),這樣就必須確保操作系統(tǒng)能夠及時(shí)的將定時(shí)器線程調(diào)度回運(yùn)行狀態(tài)。默認(rèn)情況下,Windows 的系統(tǒng)定時(shí)器精度是 15.625ms,也就是說(shuō)時(shí)間切片是這個(gè)尺寸。如果線程阻塞,出讓其時(shí)間片進(jìn)行等待,再被調(diào)度運(yùn)行的時(shí)間至少是一個(gè)切片 15.625ms。那么必須減少時(shí)間切片的長(zhǎng)度,才有可能實(shí)現(xiàn)更高的精度。
可以通過(guò)系統(tǒng) API timeBeginPeriod來(lái)修改系統(tǒng)定時(shí)器精度到 1ms(它內(nèi)部使用了沒(méi)有給出文檔的NtSetTimerResolution,這個(gè) API 可以修改到 0.5ms)。不需要的時(shí)候使用timeEndPeriod還原。
修改系統(tǒng)定時(shí)器精度有副作用。它會(huì)增加上下文切換的開(kāi)銷(xiāo),增加耗電量,降低系統(tǒng)整體性能。然而,很多程序都不得不這么做,因?yàn)闆](méi)有其它方式能獲得更高的定時(shí)器精度。比如基于 WPF 的程序(包括 Visual Studio)、使用 Chromium 內(nèi)核的應(yīng)用(Chrome、QQ)、多媒體 器、游戲等等很多程序都會(huì)在一定時(shí)間內(nèi)把系統(tǒng)定時(shí)器精度修改到 1ms。(查看方法見(jiàn)后面)
所以實(shí)際上這個(gè)副作用在桌面環(huán)境已經(jīng)成為常態(tài)。而且從 Windows 8 開(kāi)始,這個(gè)副作用減弱了。
在 1ms 的系統(tǒng)定時(shí)器精度前提下,可以使用三種方式實(shí)現(xiàn)阻塞等待:
(1)Thread.Sleep
(2)WaitHandle.WaitOne
(3)Socket.Poll
另外,多媒體定時(shí)器timeSetEvent也使用了阻塞的方式。
(1)Thread.Sleep
它的參數(shù)使用毫秒單位,所以最多只能精確到 1ms。不過(guò)事實(shí)上很不穩(wěn)定,Thread.Sleep(1)會(huì)在 1ms 與 2ms 兩種狀態(tài)間跳動(dòng),也就是可能會(huì)產(chǎn)生 +1ms 多的誤差。
實(shí)測(cè)發(fā)現(xiàn),沒(méi)有任務(wù)負(fù)載的情況下(純粹循環(huán)調(diào)用Sleep(1)),阻塞時(shí)長(zhǎng)穩(wěn)定在 2ms;而有任務(wù)負(fù)載時(shí),則至少會(huì)阻塞 1ms。這和其它兩種阻塞方式不同,詳見(jiàn)后文。
如果需要修正這個(gè)誤差,可以在阻塞 n 毫秒時(shí),使用Sleep(n-1),并通過(guò)Stopwatch計(jì)時(shí),剩余等待時(shí)間用Sleep(0)、Thread.Yield或自旋來(lái)補(bǔ)充。
Sleep(0)會(huì)出讓剩余的 CPU 時(shí)間片給優(yōu)先級(jí)相同的線程,而Thread.Yield是出讓剩余的 CPU 時(shí)間片給運(yùn)行在同一核心上的線程。在出讓的時(shí)間片結(jié)束后,其會(huì)被重新調(diào)度。一般情況下,整個(gè)過(guò)程可以在 1ms 之內(nèi)完成。
Thread.Sleep(0)和Thread.Yield在 CPU 高負(fù)載情況下非常不穩(wěn)定,實(shí)測(cè)可能會(huì)阻塞高達(dá) 6ms 時(shí)間,所以可能會(huì)產(chǎn)生更多的誤差。因此誤差修正最好通過(guò)自旋方式實(shí)現(xiàn)。
(2)WaitHandle.WaitOne
WaitHandle.WaitOne與Thread.Sleep類(lèi)似,參數(shù)也是毫秒單位。
不同之處是,沒(méi)有任務(wù)負(fù)載的情況下(純粹循環(huán)調(diào)用WaitOne(1)),阻塞時(shí)長(zhǎng)穩(wěn)定在 1.5ms;而有任務(wù)負(fù)載時(shí),則可能僅阻塞近乎于 0 的時(shí)間(猜測(cè)是它僅阻塞到當(dāng)前時(shí)間片結(jié)束,尚未找到具體的文檔說(shuō)明)。所以它阻塞的時(shí)長(zhǎng)范圍是 0 到 2ms 多。
WaitHandle.WaitOne(0)是用來(lái)測(cè)試等待句柄狀態(tài)的,它并不阻塞,所以用它來(lái)進(jìn)行誤差修正類(lèi)似于自旋,但不如直接使用自旋可靠。
(3)Socket.Poll
Socket.Poll方法的參數(shù)是以微秒為單位,理論上,它是使用了網(wǎng)卡的硬件來(lái)定時(shí),精度很高。然而,由于阻塞的實(shí)現(xiàn)仍然要依賴(lài)線程,所以它也只能達(dá)到 1ms 的精度。
它的優(yōu)勢(shì)是比Thread.Sleep和WaitHandle.WaitOne要更穩(wěn)定,誤差也更小,可以不需要修正,但要占用一個(gè) Socket 端口。
沒(méi)有任務(wù)負(fù)載的情況下(純粹循環(huán)調(diào)用Poll(1)),阻塞時(shí)長(zhǎng)穩(wěn)定在 1ms;而有任務(wù)負(fù)載時(shí),則和WaitOne類(lèi)似,可能僅阻塞近乎于 0 的時(shí)間。所以它阻塞的時(shí)長(zhǎng)范圍是 0 到 1ms 多。
Socket.Poll(0)是用來(lái)測(cè)試 Socket 狀態(tài)的,但它會(huì)阻塞,而且可能阻塞高達(dá) 6ms,所以不能用它來(lái)進(jìn)行誤差修正。
2.3timeSetEvent
timeSetEvent和之前提到的timeBeginPeriod一樣屬于 winmm.dll 提供的多媒體定時(shí)器功能。它可以直接當(dāng)作定時(shí)器使用,也是提供 1ms 的精度。在不需要的時(shí)候使用timeKillEvent來(lái)關(guān)閉。
它的穩(wěn)定性和精度也很高,如果需要 1ms 的定時(shí),而又不能使用自旋,那么這是最理想的方案。
雖然 MSDN 上說(shuō)timeSetEvent是個(gè)過(guò)時(shí)的方法,應(yīng)該用CreateTimerQueueTimer替換。但是CreateTimerQueueTimer精度和穩(wěn)定性都不如多媒體定時(shí)器,所以在需要高精度的時(shí)候,只能使用timeSetEvent。
3、定時(shí)器實(shí)現(xiàn)
需要注意的是,無(wú)論自旋還是阻塞,顯然定時(shí)器都應(yīng)該運(yùn)行在獨(dú)立的線程,不能干擾使用方線程工作。而對(duì)于高精度定時(shí)器來(lái)說(shuō),觸發(fā)事件以執(zhí)行任務(wù)的線程一般都在定時(shí)器線程內(nèi),而不是再使用獨(dú)立的任務(wù)線程。
這是因?yàn)楦呔榷〞r(shí)場(chǎng)景下,執(zhí)行任務(wù)的時(shí)間開(kāi)銷(xiāo)很可能大于定時(shí)器的時(shí)間間隔,如果默認(rèn)就在其它線程執(zhí)行任務(wù),可能導(dǎo)致占用大量線程。所以應(yīng)該把控制權(quán)交給用戶(hù),讓用戶(hù)在需要的時(shí)候自行調(diào)度任務(wù)執(zhí)行的線程。
3.1觸發(fā)模式
由于在定時(shí)器線程執(zhí)行任務(wù),所以定時(shí)器的觸發(fā)就產(chǎn)生了三種模式。以下是它們的說(shuō)明和主循環(huán)偽代碼:
(1)固定時(shí)間框架
比如間隔 10ms,任務(wù) 7-12ms,則會(huì)按照等待 10ms 、任務(wù) 7ms、等待 3ms、任務(wù) 12ms(超時(shí) 2ms 失去同步)、任務(wù) 7ms、等待 1ms(回到同步)、任務(wù) 7ms、等待 3ms、… 進(jìn)行。就是盡量按照設(shè)定好的時(shí)間框架來(lái)執(zhí)行任務(wù),只要任務(wù)不是始終超時(shí),就可以回到原本的時(shí)間框架上。
var 下一幀時(shí)間 = 0; while(定時(shí)器開(kāi)啟) { 下一幀時(shí)間 += 間隔時(shí)間; while (當(dāng)前計(jì)時(shí) < 下一幀時(shí)間) { 等待; } 觸發(fā)任務(wù); }
(2)可推遲時(shí)間框架:
上面的例子會(huì)按照等待 10ms 、任務(wù) 7ms、等待 3ms、任務(wù) 12ms(超時(shí),推遲時(shí)間框架 2ms)、任務(wù) 7ms、等待 3ms、… 進(jìn)行。超時(shí)的任務(wù)會(huì)推遲時(shí)間框架。
var 下一幀時(shí)間 = 0; while(定時(shí)器開(kāi)啟) { 下一幀時(shí)間 += 間隔時(shí)間; if (下一幀時(shí)間 < 當(dāng)前計(jì)時(shí)) 下一幀時(shí)間 = 當(dāng)前計(jì)時(shí) while (當(dāng)前計(jì)時(shí) < 下一幀時(shí)間) { 等待; } 觸發(fā)任務(wù); }
(3)固定等待時(shí)間
上面的例子會(huì)按照等待 10ms、任務(wù) 7ms、等待 10ms、任務(wù) 12ms、等待 10ms、任務(wù) 7ms… 進(jìn)行。等待時(shí)間始終不變。
while(定時(shí)器開(kāi)啟) { var 等待開(kāi)始時(shí)間 = 當(dāng)前計(jì)時(shí); while ((當(dāng)前計(jì)時(shí) - 等待開(kāi)始時(shí)間) < 間隔時(shí)間) { 等待; } 觸發(fā)任務(wù); } // 或者: var 下一幀時(shí)間 = 0; while(定時(shí)器開(kāi)啟) { 下一幀時(shí)間 += 間隔時(shí)間; while (當(dāng)前計(jì)時(shí) < 下一幀時(shí)間) { 等待; } 觸發(fā)任務(wù); 下一幀時(shí)間 = 當(dāng)前計(jì)時(shí); }
如果使用多媒體定時(shí)器(timeSetEvent),它固定實(shí)現(xiàn)了第一種模式,而其它的等待策略能夠?qū)崿F(xiàn)全部三種模式,可以根據(jù)需求選擇。
在while循環(huán)中的等待可以使用自旋或阻塞,也可以結(jié)合它們來(lái)達(dá)到精度、穩(wěn)定性和 CPU 開(kāi)銷(xiāo)的平衡。
另外,由上面的偽代碼可以看出,這三種模式的實(shí)現(xiàn)可以統(tǒng)一,能夠做到根據(jù)情況切換。
3.2線程優(yōu)先級(jí)
最好把線程優(yōu)先級(jí)調(diào)高,以保證定時(shí)器能夠穩(wěn)定工作,減少被搶占的機(jī)會(huì)。然而需要注意,這在 CPU 資源不足時(shí)可能導(dǎo)致低優(yōu)先級(jí)線程的饑餓。也就是說(shuō)不能讓高優(yōu)先級(jí)線程去等待低優(yōu)先級(jí)線程改變狀態(tài),很有可能低優(yōu)先級(jí)線程沒(méi)有機(jī)會(huì)運(yùn)行,導(dǎo)致死鎖或類(lèi)似死鎖的狀態(tài)。(見(jiàn)一種類(lèi)似的饑餓的例子)
線程的最終優(yōu)先級(jí)和進(jìn)程的優(yōu)先級(jí)有關(guān),所以有時(shí)候也需要提高進(jìn)程優(yōu)先級(jí)(見(jiàn) C# 中的多線程系列的線程優(yōu)先級(jí)說(shuō)明)。
4、其它
還有兩點(diǎn)需要注意:
(1)線程安全:定時(shí)器在獨(dú)立線程運(yùn)行,其暴露的成員都應(yīng)該實(shí)現(xiàn)線程安全,否則在定時(shí)器運(yùn)行時(shí)調(diào)用可能會(huì)產(chǎn)生問(wèn)題。
(2)及時(shí)釋放資源:多媒體定時(shí)器、等待句柄、線程等等這些都是系統(tǒng)資源,在不需要它們的時(shí)候應(yīng)該及時(shí)釋放/銷(xiāo)毀。
如何查看系統(tǒng)定時(shí)器精度?
簡(jiǎn)單的查看可以使用Sysinternals工具包中的 ClockRes,它會(huì)顯示如下信息:
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
如果是想查看哪些程序請(qǐng)求了更高的系統(tǒng)定時(shí)器精度,那么運(yùn)行:
powercfg energy -duration 5
它會(huì)監(jiān)視系統(tǒng)能耗 5s,然后在當(dāng)前目錄生成一個(gè)energy-report.html的分析報(bào)告,可以打開(kāi)它查看。
找到里面的警告部分,會(huì)有平臺(tái)計(jì)時(shí)器分辨率:未完成的計(jì)時(shí)器請(qǐng)求(Platform Timer Resolution:Outstanding Timer Request)信息。
欄 目:C#教程
下一篇:詳解C#中的定時(shí)器Timer類(lèi)及其垃圾回收機(jī)制
本文標(biāo)題:C#中自定義高精度Timer定時(shí)器的實(shí)例教程
本文地址:http://mengdiqiu.com.cn/a1/C_jiaocheng/6540.html
您可能感興趣的文章
- 01-10C#通過(guò)反射獲取當(dāng)前工程中所有窗體并打開(kāi)的方法
- 01-10C#實(shí)現(xiàn)Winform中打開(kāi)網(wǎng)頁(yè)頁(yè)面的方法
- 01-10C#自定義簽名章實(shí)現(xiàn)方法
- 01-10C#實(shí)現(xiàn)由四周向中心縮小的窗體退出特效
- 01-10WinForm實(shí)現(xiàn)自定義右下角提示效果的方法
- 01-10C#實(shí)現(xiàn)自定義windows系統(tǒng)日志的方法
- 01-10C#自定義事件監(jiān)聽(tīng)實(shí)現(xiàn)方法
- 01-10Extjs4如何處理后臺(tái)json數(shù)據(jù)中日期和時(shí)間
- 01-10C#編程實(shí)現(xiàn)自定義熱鍵的方法
- 01-10C#中DataGridView常用操作實(shí)例小結(jié)


閱讀排行
- 1C語(yǔ)言 while語(yǔ)句的用法詳解
- 2java 實(shí)現(xiàn)簡(jiǎn)單圣誕樹(shù)的示例代碼(圣誕
- 3利用C語(yǔ)言實(shí)現(xiàn)“百馬百擔(dān)”問(wèn)題方法
- 4C語(yǔ)言中計(jì)算正弦的相關(guān)函數(shù)總結(jié)
- 5c語(yǔ)言計(jì)算三角形面積代碼
- 6什么是 WSH(腳本宿主)的詳細(xì)解釋
- 7C++ 中隨機(jī)函數(shù)random函數(shù)的使用方法
- 8正則表達(dá)式匹配各種特殊字符
- 9C語(yǔ)言十進(jìn)制轉(zhuǎn)二進(jìn)制代碼實(shí)例
- 10C語(yǔ)言查找數(shù)組里數(shù)字重復(fù)次數(shù)的方法
本欄相關(guān)
- 01-10C#通過(guò)反射獲取當(dāng)前工程中所有窗體并
- 01-10關(guān)于ASP網(wǎng)頁(yè)無(wú)法打開(kāi)的解決方案
- 01-10WinForm限制窗體不能移到屏幕外的方法
- 01-10WinForm繪制圓角的方法
- 01-10C#實(shí)現(xiàn)txt定位指定行完整實(shí)例
- 01-10WinForm實(shí)現(xiàn)仿視頻 器左下角滾動(dòng)新
- 01-10C#停止線程的方法
- 01-10C#實(shí)現(xiàn)清空回收站的方法
- 01-10C#通過(guò)重寫(xiě)Panel改變邊框顏色與寬度的
- 01-10C#實(shí)現(xiàn)讀取注冊(cè)表監(jiān)控當(dāng)前操作系統(tǒng)已
隨機(jī)閱讀
- 08-05DEDE織夢(mèng)data目錄下的sessions文件夾有什
- 04-02jquery與jsp,用jquery
- 01-10SublimeText編譯C開(kāi)發(fā)環(huán)境設(shè)置
- 01-11Mac OSX 打開(kāi)原生自帶讀寫(xiě)NTFS功能(圖文
- 01-10使用C語(yǔ)言求解撲克牌的順子及n個(gè)骰子
- 01-10delphi制作wav文件的方法
- 08-05織夢(mèng)dedecms什么時(shí)候用欄目交叉功能?
- 01-10C#中split用法實(shí)例總結(jié)
- 08-05dedecms(織夢(mèng))副欄目數(shù)量限制代碼修改
- 01-11ajax實(shí)現(xiàn)頁(yè)面的局部加載