Windows 非即時作業系統,預設時鐘中斷週期 15.625ms。三大標準 Timer (Forms.Timer, Timers.Timer, Threading.Timer) 皆建立在此基礎上,實際精度難以穩定在 10ms 以下。媒體播放、遊戲迴圈、硬體控制、效能分析等場景需要 1ms 甚至亞毫秒級 精度。
QueryPerformanceCounter / QueryPerformanceFrequencyElapsedTicks / Frequency 可得微秒級時間| 策略 | 原理 | CPU 佔用 | 精度 | 適用場景 |
|---|---|---|---|---|
| 自旋等待 | Thread.SpinWait 空轉消耗週期 |
極高 (100% 核心) | 極高 (可控) | 極短等待 (<1ms)、專用核心、即時要求 |
| 阻塞等待 | 釋放時間片,OS 排程喚醒 | 極低 | 受限於系統時鐘解析度 | 長等待、通用場景、需配合 timeBeginPeriod |
public static void SpinWaitPrecise(Stopwatch sw, long targetTicks)
{
// targetTicks = 起始 ticks + 目標持續時間 * Frequency
while (sw.ElapsedTicks < targetTicks)
{
// 每次迴圈約 10-20 週期,視 CPU 頻率動態調整
Thread.SpinWait(10);
}
}
// 使用範例:精確延遲 500 微秒
var sw = Stopwatch.StartNew();
long start = sw.ElapsedTicks;
long target = start + (long)(500 * Stopwatch.Frequency / 1_000_000.0);
SpinWaitPrecise(sw, target);
Console.WriteLine($"實際延遲:{(sw.ElapsedTicks - start) * 1_000_000.0 / Stopwatch.Frequency:F1} µs");
缺點: 佔滿一核心,筆電/虛擬機/單核環境不可行。
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
public static extern uint TimeBeginPeriod(uint uPeriod); // 設定最小週期 (ms)
[DllImport("winmm.dll", EntryPoint = "timeEndPeriod")]
public static extern uint TimeEndPeriod(uint uPeriod); // 還原
// 使用
TimeBeginPeriod(1); // 要求 1ms 系統時鐘解析度
try
{
// 執行高精度定時邏輯
}
finally
{
TimeEndPeriod(1); // 必須成對呼叫,否則系統耗電/效能永久受影響
}
副作用: 系統全域影響,增加上下文切換、耗電、降低整體效能。Chrome、VS、遊戲、播放器皆常駐 1ms,桌面端已成常態。Win8+ 副作用明顯降低。
| API | 無負載穩定值 | 有負載穩定值 | 誤差範圍 | 備註 |
|---|---|---|---|---|
Thread.Sleep(1) | ~2ms | ~1ms | +1ms | 需修正:Sleep(n-1) + 自旋補餘 |
WaitHandle.WaitOne(1) | ~1.5ms | ~0ms | 0~2ms | WaitOne(0) 不阻塞,類自旋 |
Socket.Poll(1000, SelectMode.SelectRead) | ~1ms | ~0ms | 0~1ms | 最穩定,佔用一 Socket 埠 |
[DllImport("winmm.dll")]
public static extern uint timeSetEvent(
uint uDelay, // 週期 (ms)
uint uResolution, // 解析度 (0=默認)
TimerCallback lpTimeProc, // 回調委派
uint dwUser, // 使用者資料
uint fuEvent // 事件類型
);
[DllImport("winmm.dll")]
public static extern uint timeKillEvent(uint uTimerID);
// 回調簽章
public delegate void TimerCallback(uint uTimerID, uint uMsg, uint dwUser, uint dw1, uint dw2);
// 使用
uint timerId = timeSetEvent(1, 0, MyCallback, 0, 1); // TIME_PERIODIC = 1
// ...
timeKillEvent(timerId);
優勢: OS 級排程、1ms 穩定、無佔用 CPU、API 簡單。缺點: MSDN 標記過時,建議用 CreateTimerQueueTimer,但後者精度/穩定度較差,高精度場景仍首選 timeSetEvent。
// 1. 固定時間框架:嚴格按時間點觸發,任務超時不補償,下一幀對齊原框架
nextFrame += interval;
while (now < nextFrame) Wait();
Trigger();
// 2. 可推遲時間框架:任務超時推遲後續框架
nextFrame += interval;
if (nextFrame < now) nextFrame = now; // 推遲
while (now < nextFrame) Wait();
Trigger();
// 3. 固定等待間隔:任務結束後固定等待,不對齊絕對時間
waitStart = now;
while (now - waitStart < interval) Wait();
Trigger();
public void HybridWait(Stopwatch sw, long targetTicks)
{
long remaining = targetTicks - sw.ElapsedTicks;
// 剩餘 > 2ms:阻塞等待 (釋放 CPU)
if (remaining > 2 * TicksPerMs)
{
int sleepMs = (int)(remaining / TicksPerMs) - 1; // 預留 1ms 緩衝
Thread.Sleep(sleepMs);
}
// 剩餘 <= 2ms:自旋等待 (精度可控)
while (sw.ElapsedTicks < targetTicks)
{
Thread.SpinWait(10);
}
}
var thread = new Thread(TimerLoop)
{
Priority = ThreadPriority.Highest, // 或 AboveNormal
IsBackground = true
};
// 進程優先級同步提升
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
警告: 高優先級可能導致低優先級執行緒飢餓,切勿在高優先級執行緒等待低優先級資源 (鎖、UI 訊息等)。
| 項目 | 必做事項 |
|---|---|
| 執行緒安全 | 所有公開成員加鎖或用 Interlocked/Concurrent 集合 |
| 資源釋放 | 實作 IDisposable,釋放 Timer、Thread、WaitHandle、Socket、timeKillEvent |
| 例外處理 | 回調內部 try-catch,避免摧毀定時器執行緒 |
| 取消機制 | 提供 CancellationToken 或 volatile bool 停止旗標 |
| 精度驗證 | 用 ClockRes (Sysinternals) 確認系統解析度 |
// 查看目前系統時鐘解析度
> ClockRes.exe
Maximum timer interval: 15.625 ms
Minimum timer interval: 0.500 ms
Current timer interval: 1.000 ms <-- 成功被提升至 1ms
// 查詢哪些程序要求高解析度
> powercfg /energy /duration 5
// 產生 energy-report.html,搜尋 "Platform Timer Resolution:Outstanding Timer Request"
高精度定時器 = Stopwatch 計時 + timeBeginPeriod(1) 提升系統解析度 + 混合阻塞/自旋等待 + 獨立高優先級執行緒。
timeSetEvent 是 1ms 級最省力方案;自製混合循環 可達微秒級但佔用核心。
鐵律: 系統非即時,任何方案都可能失準,業務邏輯不可 完全依賴定時器觸發時間,必須設計「失去同步時的補償/檢測機制」。