.NET中實現(xiàn)高精度定時器的思路
.NET中有多少種定時器一文介紹過.NET中至少有6種定時器,但精度都不是特別高,一般在15ms~55ms之間。在一些特殊場景,可能需要高精度的定時器,這就需要我們自己實現(xiàn)了。本文將討論高精度定時器實現(xiàn)的思路。
高精度定時器
一個定時器至少需要考慮三部分功能:計時、等待、觸發(fā)模式。計時是進行時間檢查,調(diào)整等待的時間;等待則是用來跳過指定的時間間隔。觸發(fā)模式是指定時器每次Tick的時間固定還是每次定時任務(wù)時間間隔固定。比如定時器時間間隔10ms,定時任務(wù)耗時7ms,是每隔10ms觸發(fā)一次定時任務(wù),還是等定時任務(wù)執(zhí)行完后等10ms再觸發(fā)下一個定時任務(wù)。
計時
Windows提供了可用于獲取高精度時間戳或者測量時間間隔的API。系統(tǒng)原生API是QueryPerformanceCounter (QPC)
。在.NET種提供了System.Diagnostics.Stopwatch
類獲取高精度時間戳,它內(nèi)部也是通過QueryPerformanceCounter (QPC)
進行高精度計時。QueryPerformanceCounter (QPC)
使用硬件計數(shù)器作為其基礎(chǔ)。硬件計時器由三個部分組成:時鐘周期生成器、計數(shù)時鐘周期的計數(shù)器和檢索計數(shù)器值的方法。這三個分量的特征決定了QueryPerformanceCounter (QPC)
的分辨率、精度、準(zhǔn)確性和穩(wěn)定性[1]。它的精度可以高達(dá)幾十納秒,用來實現(xiàn)高精度定時器基本沒什么問題。
等待
等待策略通常有兩種:
- 自旋:讓CPU空轉(zhuǎn)等待,一直占用CPU時間。
- 阻塞:讓線程進入阻塞狀態(tài),出讓CPU時間片,滿足等待時間后切換回運行狀態(tài)。
自旋等待
自旋等待可以使用Thread.SpinWait(int iteration)
來實現(xiàn),參數(shù)iteration
是迭代次數(shù)。由于CPU速度可能是動態(tài)的,所以很難根據(jù)iteration
計算消耗的時間,最好是結(jié)合Stopwatch
使用:
void Spin(Stopwatch w, int duration) { var current = w.ElapsedMilliseconds; while ((w.ElapsedMilliseconds - current) < duration) Thread.SpinWait(5); }
由于自旋是以消耗CPU為代價的,上述代碼運行時,CPU處于滿負(fù)荷工作狀態(tài)(使用率持續(xù)保持100%左右),因此短暫的等待可以考慮自旋,長時間運行的定時器不太建議使用該方法。
阻塞等待
阻塞等待需要操作系統(tǒng)能夠及時把定時器線程調(diào)度回運行狀態(tài)。默認(rèn)情況下,Windows的系統(tǒng)的計時器精度為15ms左右。如果是線程阻塞,出讓其時間片進行等待,然后再被調(diào)度運行的時間至少是一個時間切片15ms左右。要通過阻塞實現(xiàn)高精度計時,則需要減少時間切片的長度。Windows系統(tǒng)API提供了timeEndPeriod
可以把計時器精度修改到1ms,在使用計時器服務(wù)之前立即調(diào)用timeEndPeriod
,并在使用完計時器服務(wù)后立即調(diào)用timeEndPeriod
。timeEndPeriod
和timeEndPeriod
必須成對出現(xiàn)。
在Windows 10, version 2004之前,
timeEndPeriod
會影響全局Windows設(shè)置,所有進程都會使用修改后的計時精度。從Windows 10, version 2004開始,只有調(diào)用timeEndPeriod
的進程收到影響。
設(shè)置更高的精度可以提高等待函數(shù)中超時間隔的準(zhǔn)確性。 但是,它也可能會降低整體系統(tǒng)性能,因為線程計劃程序更頻繁地切換任務(wù)。 高精度還可以阻止 CPU 電源管理系統(tǒng)進入節(jié)能模式。 設(shè)置更高的分辨率不會提高高分辨率性能計數(shù)器的準(zhǔn)確性。
通常我們使用Thread.Sleep來掛起線程等待,Sleep的參數(shù)最小為1ms,但實際上很不穩(wěn)定,實測發(fā)現(xiàn)大部分時候穩(wěn)定在阻塞2ms。我們可以采用Sleep(0)或者Thread.Yield
結(jié)合Stopwatch
計時的方式修正。
void wait(Stopwatch w, int duration) { var current = w.ElapsedMilliseconds; while ((w.ElapsedMilliseconds - current) < duration) Thread.Sleep(0); }
Thread.Sleep(0)和Thread.Yield在 CPU 高負(fù)載情況下非常不穩(wěn)定,可能會產(chǎn)生更多的誤差。因此誤差修正最好通過自旋方式實現(xiàn)。
還有一種阻塞的方式是多媒體定時器timeSetEvent
,也是網(wǎng)上關(guān)于高精度定時器提得比較多的一種方式。它是winmm.dll
中的函數(shù),穩(wěn)定性和精度都比較高,能提供1ms的精度。
官方文檔中說timeSetEvent
是一個過時的方法,建議使用CreateTimerQueueTimer
替代[3]。但CreateTimerQueueTimer
的精度和穩(wěn)定性都不如多媒體定時器,所以在需要高精度定時器時,還是要用timeSetEvent
。以下是封裝多媒體定時器的例子
public enum TimerError { MMSYSERR_NOERROR = 0, MMSYSERR_ERROR = 1, MMSYSERR_INVALPARAM = 11, MMSYSERR_NOCANDO = 97, } public enum RepeateType { TIME_ONESHOT=0x0000, TIME_PERIODIC = 0x0001 } public enum CallbackType { TIME_CALLBACK_FUNCTION = 0x0000, TIME_CALLBACK_EVENT_SET = 0x0010, TIME_CALLBACK_EVENT_PULSE = 0x0020, TIME_KILL_SYNCHRONOUS = 0x0100 } public class HighPrecisionTimer { private delegate void TimerCallback(int id, int msg, int user, int param1, int param2); [DllImport("winmm.dll", EntryPoint = "timeGetDevCaps")] private static extern TimerError TimeGetDevCaps(ref TimerCaps ptc, int cbtc); [DllImport("winmm.dll", EntryPoint = "timeSetEvent")] private static extern int TimeSetEvent(int delay, int resolution, TimerCallback callback, int user, int eventType); [DllImport("winmm.dll", EntryPoint = "timeKillEvent")] private static extern TimerError TimeKillEvent(int id); private static TimerCaps _caps; private int _interval; private int _resolution; private TimerCallback _callback; private int _id; static HighPrecisionTimer() { TimeGetDevCaps(ref _caps, Marshal.SizeOf(_caps)); } public HighPrecisionTimer() { Running = false; _interval = _caps.periodMin; _resolution = _caps.periodMin; _callback = new TimerCallback(TimerEventCallback); } ~HighPrecisionTimer() { TimeKillEvent(_id); } public int Interval { get { return _interval; } set { if (value < _caps.periodMin || value > _caps.periodMax) throw new Exception("invalid Interval"); _interval = value; } } public bool Running { get; private set; } public event Action Ticked; public void Start() { if (!Running) { _id = TimeSetEvent(_interval, _resolution, _callback, 0, (int)RepeateType.TIME_PERIODIC | (int)CallbackType.TIME_KILL_SYNCHRONOUS); if (_id == 0) throw new Exception("failed to start Timer"); Running = true; } } public void Stop() { if (Running) { TimeKillEvent(_id); Running = false; } } private void TimerEventCallback(int id, int msg, int user, int param1, int param2) { Ticked?.Invoke(); } }
觸發(fā)模式
由于定時任務(wù)執(zhí)行時間不確定,并且可能耗時超過定時時間間隔,定時器的觸發(fā)可能會有三種模式:固定時間框架,可推遲時間框架,固定等待時間。
- 固定時間框架:盡量按照設(shè)定的時間來執(zhí)行任務(wù),只要任務(wù)不是始終超時,就可以回到原來的時間框架上
- 可推遲時間框架:也是盡量按照設(shè)定的時間執(zhí)行任務(wù),但是超時的任務(wù)會推遲時間框架。
- 固定等待時間:不管任務(wù)執(zhí)行時長,每次任務(wù)執(zhí)行結(jié)束到下一次任務(wù)開始執(zhí)行間的等待時間固定。
假定時間間隔為10ms,任務(wù)執(zhí)行的時間在7~11ms之間,下圖中顯示了三種觸發(fā)模式的區(qū)別。
其實還有一種觸發(fā)模式:任務(wù)執(zhí)行時長大于時間間隔時,只要時間間隔一到,就執(zhí)行定時任務(wù),多個定時任務(wù)并發(fā)執(zhí)行。之所以這里沒有提及這種模式,是因為在高精度定時場景中,執(zhí)行任務(wù)的時間開銷很有可能大于定時器的時間間隔,如果開啟新線程執(zhí)行定時任務(wù),可能會占用大量線程,這個需要結(jié)合實際情況考慮如何執(zhí)行定時任務(wù)。這里討論的是默認(rèn)在定時器線程上執(zhí)行定時任務(wù)。
到此這篇關(guān)于.NET中如何實現(xiàn)高精度定時器的文章就介紹到這了,更多相關(guān).NET定時器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
asp下實現(xiàn)截取字符串特定部分內(nèi)容函數(shù)
asp下實現(xiàn)截取字符串特定部分內(nèi)容函數(shù)...2007-08-08PostHttpPage用asp是實現(xiàn)模擬登錄效果的代碼
PostHttpPage用asp是實現(xiàn)模擬登錄效果的代碼...2007-09-09