.NET中如何实现高精度定时器
.NET中有多少種定時(shí)器一文介紹過(guò).NET中至少有6種定時(shí)器,但精度都不是特別高,一般在15ms~55ms之間。在一些特殊場(chǎng)景,可能需要高精度的定時(shí)器,這就需要我們自己實(shí)現(xiàn)了。本文將討論高精度定時(shí)器實(shí)現(xiàn)的思路。
高精度定時(shí)器
一個(gè)定時(shí)器至少需要考慮三部分功能:計(jì)時(shí)、等待、觸發(fā)模式。計(jì)時(shí)是進(jìn)行時(shí)間檢查,調(diào)整等待的時(shí)間;等待則是用來(lái)跳過(guò)指定的時(shí)間間隔。觸發(fā)模式是指定時(shí)器每次Tick的時(shí)間固定還是每次定時(shí)任務(wù)時(shí)間間隔固定。比如定時(shí)器時(shí)間間隔10ms,定時(shí)任務(wù)耗時(shí)7ms,是每隔10ms觸發(fā)一次定時(shí)任務(wù),還是等定時(shí)任務(wù)執(zhí)行完后等10ms再觸發(fā)下一個(gè)定時(shí)任務(wù)。
計(jì)時(shí)
Windows提供了可用于獲取高精度時(shí)間戳或者測(cè)量時(shí)間間隔的API。系統(tǒng)原生API是QueryPerformanceCounter (QPC)。在.NET種提供了System.Diagnostics.Stopwatch類獲取高精度時(shí)間戳,它內(nèi)部也是通過(guò)QueryPerformanceCounter (QPC)進(jìn)行高精度計(jì)時(shí)。QueryPerformanceCounter (QPC)使用硬件計(jì)數(shù)器作為其基礎(chǔ)。硬件計(jì)時(shí)器由三個(gè)部分組成:時(shí)鐘周期生成器、計(jì)數(shù)時(shí)鐘周期的計(jì)數(shù)器和檢索計(jì)數(shù)器值的方法。這三個(gè)分量的特征決定了QueryPerformanceCounter (QPC)的分辨率、精度、準(zhǔn)確性和穩(wěn)定性[1]。它的精度可以高達(dá)幾十納秒,用來(lái)實(shí)現(xiàn)高精度定時(shí)器基本沒(méi)什么問(wèn)題。
等待
等待策略通常有兩種:
- 自旋:讓CPU空轉(zhuǎn)等待,一直占用CPU時(shí)間。
- 阻塞:讓線程進(jìn)入阻塞狀態(tài),出讓CPU時(shí)間片,滿足等待時(shí)間后切換回運(yùn)行狀態(tài)。
自旋等待
自旋等待可以使用Thread.SpinWait(int iteration)來(lái)實(shí)現(xiàn),參數(shù)iteration是迭代次數(shù)。由于CPU速度可能是動(dòng)態(tài)的,所以很難根據(jù)iteration計(jì)算消耗的時(shí)間,最好是結(jié)合Stopwatch使用:
void Spin(Stopwatch w, int duration)
{
var current = w.ElapsedMilliseconds;
while ((w.ElapsedMilliseconds - current) < duration)
Thread.SpinWait(5);
}
由于自旋是以消耗CPU為代價(jià)的,上述代碼運(yùn)行時(shí),CPU處于滿負(fù)荷工作狀態(tài)(使用率持續(xù)保持100%左右),因此短暫的等待可以考慮自旋,長(zhǎng)時(shí)間運(yùn)行的定時(shí)器不太建議使用該方法。
阻塞等待
阻塞等待需要操作系統(tǒng)能夠及時(shí)把定時(shí)器線程調(diào)度回運(yùn)行狀態(tài)。默認(rèn)情況下,Windows的系統(tǒng)的計(jì)時(shí)器精度為15ms左右。如果是線程阻塞,出讓其時(shí)間片進(jìn)行等待,然后再被調(diào)度運(yùn)行的時(shí)間至少是一個(gè)時(shí)間切片15ms左右。要通過(guò)阻塞實(shí)現(xiàn)高精度計(jì)時(shí),則需要減少時(shí)間切片的長(zhǎng)度。Windows系統(tǒng)API提供了timeEndPeriod可以把計(jì)時(shí)器精度修改到1ms,在使用計(jì)時(shí)器服務(wù)之前立即調(diào)用timeEndPeriod,并在使用完計(jì)時(shí)器服務(wù)后立即調(diào)用timeEndPeriod。timeEndPeriod和timeEndPeriod必須成對(duì)出現(xiàn)。
在Windows 10, version 2004之前,
timeEndPeriod會(huì)影響全局Windows設(shè)置,所有進(jìn)程都會(huì)使用修改后的計(jì)時(shí)精度。從Windows 10, version 2004開(kāi)始,只有調(diào)用timeEndPeriod的進(jìn)程收到影響。
設(shè)置更高的精度可以提高等待函數(shù)中超時(shí)間隔的準(zhǔn)確性。 但是,它也可能會(huì)降低整體系統(tǒng)性能,因?yàn)榫€程計(jì)劃程序更頻繁地切換任務(wù)。 高精度還可以阻止 CPU 電源管理系統(tǒng)進(jìn)入節(jié)能模式。 設(shè)置更高的分辨率不會(huì)提高高分辨率性能計(jì)數(shù)器的準(zhǔn)確性。[2]
通常我們使用Thread.Sleep來(lái)掛起線程等待,Sleep的參數(shù)最小為1ms,但實(shí)際上很不穩(wěn)定,實(shí)測(cè)發(fā)現(xiàn)大部分時(shí)候穩(wěn)定在阻塞2ms。我們可以采用Sleep(0)或者Thread.Yield結(jié)合Stopwatch計(jì)時(shí)的方式修正。
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)定,可能會(huì)產(chǎn)生更多的誤差。因此誤差修正最好通過(guò)自旋方式實(shí)現(xiàn)。
還有一種阻塞的方式是多媒體定時(shí)器timeSetEvent,也是網(wǎng)上關(guān)于高精度定時(shí)器提得比較多的一種方式。它是winmm.dll中的函數(shù),穩(wěn)定性和精度都比較高,能提供1ms的精度。
官方文檔中說(shuō)timeSetEvent是一個(gè)過(guò)時(shí)的方法,建議使用CreateTimerQueueTimer替代[3]。但CreateTimerQueueTimer的精度和穩(wěn)定性都不如多媒體定時(shí)器,所以在需要高精度定時(shí)器時(shí),還是要用timeSetEvent。以下是封裝多媒體定時(shí)器的例子
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ā)模式
由于定時(shí)任務(wù)執(zhí)行時(shí)間不確定,并且可能耗時(shí)超過(guò)定時(shí)時(shí)間間隔,定時(shí)器的觸發(fā)可能會(huì)有三種模式:固定時(shí)間框架,可推遲時(shí)間框架,固定等待時(shí)間。
- 固定時(shí)間框架:盡量按照設(shè)定的時(shí)間來(lái)執(zhí)行任務(wù),只要任務(wù)不是始終超時(shí),就可以回到原來(lái)的時(shí)間框架上
- 可推遲時(shí)間框架:也是盡量按照設(shè)定的時(shí)間執(zhí)行任務(wù),但是超時(shí)的任務(wù)會(huì)推遲時(shí)間框架。
- 固定等待時(shí)間:不管任務(wù)執(zhí)行時(shí)長(zhǎng),每次任務(wù)執(zhí)行結(jié)束到下一次任務(wù)開(kāi)始執(zhí)行間的等待時(shí)間固定。
假定時(shí)間間隔為10ms,任務(wù)執(zhí)行的時(shí)間在7~11ms之間,下圖中顯示了三種觸發(fā)模式的區(qū)別。
其實(shí)還有一種觸發(fā)模式:任務(wù)執(zhí)行時(shí)長(zhǎng)大于時(shí)間間隔時(shí),只要時(shí)間間隔一到,就執(zhí)行定時(shí)任務(wù),多個(gè)定時(shí)任務(wù)并發(fā)執(zhí)行。之所以這里沒(méi)有提及這種模式,是因?yàn)樵诟呔榷〞r(shí)場(chǎng)景中,執(zhí)行任務(wù)的時(shí)間開(kāi)銷很有可能大于定時(shí)器的時(shí)間間隔,如果開(kāi)啟新線程執(zhí)行定時(shí)任務(wù),可能會(huì)占用大量線程,這個(gè)需要結(jié)合實(shí)際情況考慮如何執(zhí)行定時(shí)任務(wù)。這里討論的是默認(rèn)在定時(shí)器線程上執(zhí)行定時(shí)任務(wù)。
-
https://learn.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps#low-level-hardware-clock-characteristics ??
-
https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod?redirectedfrom=MSDN ??
-
https://learn.microsoft.com/en-us/previous-versions//dd757634(v=vs.85)?redirectedfrom=MSDN ??
總結(jié)
以上是生活随笔為你收集整理的.NET中如何实现高精度定时器的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 讯景 RX 7600 显卡海外上架:频率
- 下一篇: 【Dotnet 工具箱】推荐一个 Flu