Windows 上為什麼應先用事件等待而不是計時器等待 - 避免以約 15.6ms 粒度做輪詢
· 小村 豪 · Windows 開發, 同步, 事件, 計時器, 設計
上次在Windows 軟即時實務指南已經提過要避免依賴 Sleep 的週期迴圈。
這一次把焦點收束到一個重點:為什麼短的 timer wait 不如 event wait。
在 Windows 上,若用 Sleep(1) 或帶短 timeout 的 wait 去做「每隔一段時間看看」,終究會被 system clock 的粒度 與 之後的排程延遲 影響。
一般設定下 platform timer resolution 往往落在 15.6ms 級,所以即使你心裡想「1ms 後再看一次」,實際上的等待相當粗糙。
反過來,若真正要等的是工作抵達、I/O 完成、停止請求、狀態變化——這些是「事件」而不是「時間」——就沒必要固定間隔去輪詢。
由事件發生方做 signal,等待方去等 event,對延遲、對 CPU、對耗電都更順。
本文以下列問題來展開。
- 為什麼
Sleep(1)或短的 timer wait 不如你想像的那麼準 - 為什麼 event wait 比較不受這些限制
- 什麼場景下該選 event 而不是 timer
- 即便如此,什麼時候仍該用 timer
1. 先下結論
- 要等工作抵達或 I/O 完成時,應該等 event,不要等 timer。
- Windows 的 timed wait 無法擺脫 system clock 粒度的影響。
Sleep(1)的意思不是「1ms 後準時醒來」。- 而且 timeout 到了,thread 也只是變成 ready,並不保證立即開跑。
- 所以 「其實要等事件,卻用 timer 去看看」的設計,對延遲與耗電都不利。
- timer 應該保留給 真正以時間為條件 的場景。
用實務上的講法,差不多就是:
- 「每 5 秒送 metrics」 -> timer 的工作
- 「佇列有工作就立刻跑」 -> event / semaphore / condition variable /
WaitOnAddress的工作 - 「I/O 完成接著跑後續」 -> completion / event 的工作
- 「收到停止請求就停」 -> stop event / cancellation 的工作
2. 哪裡有問題
2.1 timed wait 受 system clock 粒度限制
Windows 的 wait functions 其 timeout 的精度取決於 system clock resolution。
Sleep 也一樣,指定的毫秒數並不會剛剛好以「你指定的長度」保證。
要記住的是:你給 1ms,不代表 1ms 後就會醒。
2.2 到了時間,也不保證立刻跑
更麻煩的是,timeout 到了之後,thread 並不會立刻被執行。
就像 Sleep 的文件所說,等待結束後 thread 會變成 ready,但 能不能馬上拿到 CPU 去跑,沒保證。
還會被其他 thread、priority、CPU 的 idle state、DPC / ISR、鎖競爭等影響。
也就是說,短的 timer wait 至少有兩層不確定性:
- timeout 的判定本身就被 timer 粒度拉走
- timeout 之後能不能馬上跑,要看 scheduler 臉色
2.3 Sleep(1) 不等於「1ms 週期」
看到 Sleep(1) 直覺會想「這是每 1ms 跑一次的迴圈」。
但這樣讀是不對的。
while (!g_stop)
{
Step();
Sleep(1);
}
這個迴圈的實際狀況是:
- 每次都要加上
Step()的執行時間 Sleep(1)的等待本身被粒度拉走- 即使醒來也不一定能馬上跑
3. 為什麼 event 等待比較有利
3.1 等待結束條件從「時間到」改成「signal」
event wait 的優勢在於,等待的意義本身變了。
timer wait 的邏輯是:
- 就算什麼都沒發生
- 時間到就醒來
- 醒來後再看「有沒有發生什麼」
event wait 的邏輯是:
- 事情發生的一方去 signal
- 有 signal 才算等到
- 醒來的瞬間就已經有原因
flowchart LR
start["正在等待的 thread"] --> q{"真正在等的是什麼?"}
q -- "時間到" --> timer["timer / waitable timer"]
q -- "工作抵達" --> event["event / semaphore / condition variable"]
q -- "值的變化" --> addr["WaitOnAddress"]
q -- "I/O 完成" --> io["completion / event"]
q -- "停止請求" --> stop["stop event / cancellation"]
3.2 依「要等什麼」來選工具
第一層判斷,差不多用下表就夠。
| 要等什麼 | 不好的寫法 | 首選 |
|---|---|---|
| 佇列出現工作 | 用 Sleep(1) 去 TryPop |
event / semaphore |
| I/O 完成 | 用 timer 看狀態 | overlapped I/O 的 event / IOCP |
| 停止請求 | 每 100ms 看 stop flag | stop event / cancellation |
| 同一行程內的值變化 | while (flag == 0) Sleep(1) |
WaitOnAddress |
| 時間到 | 硬用 event 湊 | timer / waitable timer |
3.3 event 也不是萬靈丹
event wait 的優勢在於 不必被 timer 粒度喚醒,但並不代表 被 signal 的瞬間就絕對零延遲起跑。
event wait 實際上仍會受這些影響:
- scheduler latency
- thread priority
- CPU 的 power state
- 鎖競爭
- page fault
- DPC / ISR
但至少 「撐到下一個 timer tick 才醒」這種多餘的等待,可以拿掉。
4. 典型反模式
4.1 用 Sleep(1) 輪詢佇列
最常見的就是這種:
for (;;)
{
if (g_stop)
{
break;
}
WorkItem item;
if (TryPop(item))
{
Process(item);
continue;
}
Sleep(1);
}
乍看很單純,但問題有 3 個:
- 佇列空也會定期醒來
- latency 被 timer 粒度拉走
- 電力面上也是損
4.2 用 Thread.Sleep(1) / Task.Delay(1) 監看狀態
C# / .NET 裡也會有同樣的味道:
while (!stoppingToken.IsCancellationRequested)
{
if (_queue.TryDequeue(out WorkItem? item))
{
await ProcessAsync(item, stoppingToken);
continue;
}
await Task.Delay(1, stoppingToken);
}
外觀是 async、看起來很溫順,但本質上仍是 polling。
5. 要這樣修正
5.1 由 producer 在抵達時 signal
若是等佇列抵達,改成 由 producer signal,而不是 polling。
- producer 把 item 放進佇列
- 放入後立刻
SetEvent - consumer 以
WaitForSingleObject或WaitForMultipleObjects等 - 醒來後把 queue drain 掉
5.2 用 WaitForMultipleObjects 同時等 work 與 stop
簡單的 worker,用下列形狀最好理解。
HANDLE waits[2] = { _stopEvent, _workEvent };
for (;;)
{
DWORD rc = WaitForMultipleObjects(2, waits, FALSE, INFINITE);
if (rc == WAIT_OBJECT_0)
{
return;
}
if (rc != WAIT_OBJECT_0 + 1)
{
throw std::runtime_error("WaitForMultipleObjects failed.");
}
DrainQueue();
}
這個例子有 3 個重點:
Sleep(1)消失了- item 抵達時由 producer
SetEvent - worker 同時等
stop與work
5.3 同一行程內,WaitOnAddress 也可以考慮
如果只是在同一行程內「等某個值變化」,WaitOnAddress 也是相當有力的選擇。
大致的分界感是:
- 跨行程或一般的等待對象 -> event / semaphore / waitable object
- 同一行程的輕量值變化 ->
WaitOnAddress
6. 仍然適合用 timer 的場景
6.1 條件真的是「時間」本身
當然 timer 還是有它的場景:
- 每 5 秒送 metrics
- 200ms 後 retry
- 每 1 分鐘清一次 cache
- 等到期限時間就 timeout
這時要等的 真的是時間。
6.2 用 waitable timer
在 Windows 上要等「時間」,比起亂疊 Sleep,使用 waitable timer 語意更清楚。
6.3 別把 timeBeginPeriod 當常用手段
當短 timer wait 的精度令人焦慮時,會想加一句 timeBeginPeriod(1)。
但不要把它當首選。
原因有 3 個:
- 會有 power / performance 的代價
- 近期的 Windows 行為比以前複雜一些
- 多半根本原因沒修
7. 審查時的 checklist
- 是否還在用
Sleep(1)/Thread.Sleep(1)/Task.Delay(1)寫輪詢迴圈 - 其實是在等佇列抵達、I/O 完成、停止請求,卻用 timer poll
- 是否設計成由 producer / completion 側做 signal
- 能不能用一次 wait 同時等
stop與work - 同一行程內的值變化能不能改
WaitOnAddress - 用 timer 的地方,真正要等的是不是「時間」
8. 總結
在 Windows 上,用短 timer wait 做「每隔一段時間看看」的設計,一定會被 timer 粒度與 scheduler 拉走。
因此 Sleep(1) 或短 timeout 看似精準,實際上並不是。
反過來,若真正要等的是工作抵達、I/O 完成、停止請求、狀態變化這類「事件」,event wait 就自然得多。
簡短歸納一句:
等時間用 timer,等事件用 event。
只要這條界線清楚:
- latency 變得好讀
- 減少多餘的週期性 wakeup
- 電力面也會好一些
- 程式碼的意圖更清楚
效果就會浮現出來。
9. 參考資料
- Sleep function (Win32)
- Wait Functions
- WaitForSingleObject function
- Event Objects (Synchronization)
- Using Event Objects
- WaitOnAddress function
- WakeByAddressSingle function
- timeBeginPeriod function
- CreateWaitableTimerExW function
- SetWaitableTimer function
- Thread.Sleep Method (.NET)
- Results for the Idle Energy Efficiency Assessment
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
發生非預期例外時的 checklist - 要讓應用結束還是繼續,先看的判斷表
本文以 C# / .NET 與 Windows 應用為前提,把非預期例外發生後該結束還是繼續的判斷拆成失敗單位、共用狀態、外部副作用、原生邊界四個觀察點,並提供判斷表與典型情境,協助讀者在 catch 之前先判斷是否還能信任應用狀態。
Windows 應用程式開發中遵守最低限度安全性的檢核表
用檢核表形式整理 WPF / WinForms / WinUI / C++ / C# 等 Windows 應用程式發佈前最低限不想漏的安全性要點。涵蓋避免不必要的管理員權限、EXE 與更新物簽章加時間戳、改用 DPAPI 與 Credential Locker、保留 HTT...
PeriodicTimer / System.Threading.Timer / DispatcherTimer 的區分使用 - 先整理 .NET 的定期執行
整理 .NET 中 PeriodicTimer、System.Threading.Timer、DispatcherTimer 的差異與使用場景,從執行緒、async 流程、callback 重疊三個角度切入,協助你在 worker、ThreadPool 背景處理及 WPF ...
把 Generic Host / BackgroundService 帶進桌面應用程式的理由 - 啟動・壽命・graceful shutdown 的整理會輕鬆很多
整理把 .NET Generic Host 與 BackgroundService 帶進 WPF / WinForms 桌面應用程式的理由,把啟動、lifetime、graceful shutdown 集中於入口管理。透過 StartAsync / ExecuteAsync...
FileSystemWatcher 的使用方法與注意事項 - 漏掉、重複通知、完成判定的陷阱
本文整理 FileSystemWatcher 的正確用法。把事件視為跡象而非完成通知,將通知摺疊為重新掃描請求,由傳送端以 temp 後 rename 明示完成,多 worker 以原子性 claim 取得所有權,最後以 full rescan 與 idempotency ...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
UI 執行緒 & 計時器
整理 WPF / WinForms UI 執行緒、非同步流程、Dispatcher 使用、計時器判斷的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
技術諮詢 & 設計審查
協助釐清設計方向、架構邊界、生命週期責任,以及既有 Windows 資產的處理方式。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。