Windows 軟即時實戰指南 - 為了減少延遲的檢查清單
這篇文章要談的不是加了特殊即時擴充的 Windows,而是 一般的 Windows 10 / 11。 對象是,在普通桌機或筆電上執行的 user-mode 一般應用程式。
這裡要追求的不是 hard real-time 的保證。 即使是普通的 Windows,只要把設計、等待方式、優先順序、電源設定、量測整合起來,就能把 soft real-time 做到相當實用的狀態。
這次不以完整列出所有項目為目標,而是做成 能立刻掌握「該重看什麼」的結構。 先看第 4 節的檢查清單,大概就能看出要重看哪裡。
1. 先說結論(一句話)
- 在普通 Windows 上,要追求的不是 hard real-time 的保證,而是縮小延遲與抖動,並減少 deadline miss
- 首先要重看的不是優先順序的值,而是 週期執行緒裡到底塞了什麼
- 每週期必經、對時間敏感的處理放進 fast path,儲存、通訊、UI 這類可以稍晚的處理歸到 slow path
- 在 fast path 中避免依賴
Sleep的等待、阻塞 I/O、每次的配置/釋放、以及無上限的佇列 - 音訊或影片這類持續串流,首先考慮 MMCSS
- 實際營運中,AC 供電、電源模式、timer resolution、power throttling、背景負載 會有影響
- 評估不要只看平均值,還要看 p99 / p99.9 / max / miss 次數 / queue 深度 / DPC / ISR / page fault
實務上大致按以下順序:
- 把週期迴圈從依賴
Sleep上拿下來 - 把 fast path 與 slow path 切開
- 把佇列改成固定長度,並先決定溢位時的方針
- 把 I/O、配置、重鎖從 fast path 移出
- 只讓必要的執行緒用優先順序或 MMCSS
- 把普通 Windows 的電源設定與量測整合到位
想掌握「在普通 Windows 上,改哪裡才比較不會延遲」,依這個順序看會最清楚。
2. 這篇文章講的「普通 Windows」是什麼
這裡說的「普通 Windows」指的是 Windows 10 / 11 的一般 PC。 不使用特殊的即時擴充或專用 OS,只在 Windows 標準 API 與設定的範圍內,看能把穩定性推到什麼程度,這是本文的立場。
flowchart TB
scope["本文的對象"] --> s1["Windows 10 / 11 的一般 PC"]
scope --> s2["user-mode 的一般應用程式"]
scope --> s3["以標準 API 與標準設定為主"]
scope --> s4["普通桌機 / 筆電"]
out["本文的對象外"] --> o1["專用 RTOS 或 RT 擴充"]
out --> o2["以核心驅動為主的控制"]
out --> o3["把嚴苛時間需求全面移交給 FPGA 或 MCU 的構成"]
2.1. 涵蓋的範圍
涵蓋的例如:
- 執行在 Windows 10 / 11 上的 C++ / C# user-mode 應用程式
- 音訊、影像、量測、裝置控制、週期處理等需要「延遲小」的軟體
- 一般桌機、工作站、筆電
也就是說,討論的是「作為能正常部署的 Windows 應用程式,能撐到多遠」。
2.2. 排除在外的
另一方面,以下在本文的主線之外:
- hard real-time 的保證
- 導入專用 RTOS 或即時擴充產品
- 以核心驅動或自製驅動為主角的設計
- 把時間敏感部分全面移到 FPGA、MCU、專用控制器
當需求更嚴苛時,這些選擇確實必要。 不過在一開始就往那邊走之前,先在普通 Windows 上累積可重現的改善 仍然相當有價值。
3. Windows 上的軟即時是什麼
3.1. 目標
在普通 Windows 上要達到的狀態如下:
- 把平時的延遲壓低
- 讓抖動變小
- 偶爾出現延遲尖峰也不會壞
- 能觀測到「沒趕上期限」的次數
這裡說的 soft real-time,不是「絕不遲到」,而是 不易遲到、遲到能被看到、就算遲到也不會壞 的思路。
3.2. 本文使用的名詞
先釐清這 3 個,讀起來會順很多。
| 名詞 | 意思 |
|---|---|
| 延遲 | 處理比預計時間晚開始,或晚結束 |
| 抖動 | 週期或處理時間的波動。無法每次都以同樣間隔運作 |
| deadline miss | 在既定期限內沒能完成處理 |
例如想每 1ms 動一次的處理,在 1.8ms 後才開始,那這 0.8ms 就是延遲。 如果不是每次都 1.0ms,而是 0.9ms、1.3ms、2.1ms 這樣搖擺,那就是抖動。
3.3. 什麼情況會開始變難
只靠普通 Windows 的 user-mode 應用程式,會開始吃緊的需求例如:
- 要保證零期限違反
- 要長時間穩定守住數百微秒以下
- 要把沉重 GUI、通訊、儲存與高頻週期處理全部擠在一起
- 用電池驅動或省電優先時也要守住嚴格週期
- 連來自驅動或裝置的尖峰也不被允許
到了這種程度,普通 Windows 單獨就會吃緊。 這時候最好考慮 只把真正對時間嚴苛的部分交給韌體、專用控制器、FPGA、另一個 RTOS。
4. 先看的檢查清單
本節是 為了先掌握「該注意哪些」而設的。 先看下面這張表,大致就能抓到要重看的位置。
4.1. 全貌
| 要確認的項目 | 首先要做 | 典型的 NG |
|---|---|---|
| 等待方式 | 以絕對期限運作。使用事件驅動或高精度 waitable timer | 以 Sleep(1) 為基礎的週期迴圈 |
| 處理分離 | 切開 fast path 與 slow path | 把儲存、送信、UI 放進 fast path |
| 佇列 | 固定長度,先定好溢位方針 | 無上限佇列把問題後拖 |
| fast path 內容 | 去掉配置、重日誌、阻塞 I/O、重鎖 | new / malloc / 同步 I/O / 每次重建字串 |
| 優先順序 | 只提高必要的執行緒;音訊/影像考慮 MMCSS | 一口氣開 REALTIME_PRIORITY_CLASS |
| OS / 電源 | 確認 AC 供電、電源模式、timer resolution、power throttling | 以電池驅動或省電模式做評估 |
| 量測 | 取 lateness、執行時間、miss 次數、queue 深度 | 只看平均值 |
flowchart TB
start["週期處理會遲到"] --> c1["確認等待方式"]
c1 --> c2["確認 fast path 是否塞了重活"]
c2 --> c3["確認佇列上限與溢位方針"]
c3 --> c4["確認優先順序與 MMCSS 的用法"]
c4 --> c5["確認電源模式與 timer / power throttling"]
c5 --> c6["還是在抖,量測 DPC / ISR / page fault"]
接下來依序看各項。
4.2. 週期迴圈不要靠 Sleep
首先要確認的就是這裡。
Sleep(1) 不是「剛好等 1ms」,而是 至少等 1ms 以上。
再加上 Step() 的執行時間,週期的誤差會直接累積。
flowchart LR
a["Sleep(1)"] --> b["可能等比預期更久"]
b --> c["加上 Step 的執行時間"]
c --> d["週期誤差累積"]
週期處理要以 絕對期限 而不是相對時間來跑,才會穩定。 概念像這樣:
int64_t next = QpcNow() + periodTicks;
while (running)
{
WaitUntil(next - wakeMarginTicks); // event / waitable timer
while (QpcNow() < next)
{
CpuRelax(); // 最後才短暫 spin
}
int64_t started = QpcNow();
FastStep(); // no blocking, no alloc, no heavy lock
int64_t finished = QpcNow();
RecordTiming(next, started, finished);
next += periodTicks;
while (finished > next)
{
++missedDeadlines;
next += periodTicks;
}
}
重點有 2 個:
- 不要每次都
next = now + period - 遲到時的處置事先決定好
想在普通 Windows 上穩定下來,這裡效益相當大。
4.3. 切開 fast path 與 slow path
就結構來說,效益最大的是 把對時間嚴苛的處理與可以稍晚的處理切開。
- fast path 每週期必經、希望很快結束的處理
- slow path 儲存、通訊、整理、彙總、UI 等可以稍晚的處理
flowchart LR
dev["裝置 / 計時器 / 事件"] --> fast["fast path:取得 / 控制 / 最小複製"]
fast --> queue["固定長度佇列"]
queue --> slow["slow path:整理 / 儲存 / 送信 / UI"]
fast --> stat["時間紀錄 / miss 紀錄"]
stat --> slow
fast path 只做下列動作:
- 取資料
- 計算控制值
- 最小限度的複製
- 時間戳記
- 丟進佇列
- 記錄 miss 或 overrun
其他交給 slow path 比較穩。 在普通 Windows 上追求軟即時,這個切分就是地基。
4.4. 佇列用固定長度,先決定溢位方針
一想到「遲到就塞佇列」,就很容易把問題往後推。 無上限佇列看似安全,實際上往往只是 把延遲藏起來。
flowchart LR
input["fast path 產生資料"] --> q{"固定長度佇列是否有空"}
q --> enq["有空:直接放入"]
q --> policy["沒空:套用方針"]
policy --> latest["最新值優先:丟掉舊資料"]
policy --> strict["不允許遺漏:錯誤 / 停止 / 警報"]
policy --> logonly["日誌用途:記錄掉落數"]
事先要決定的是這 3 件事:
- 佇列上限
- 溢位時的方針
- 如何記錄溢位事實
例如只有最新值有意義,那就丟掉舊的比較自然。 若完全不能遺漏,與其默默延後,不如當成錯誤處理,後面會省事。
4.5. 不要把重活放進 fast path
fast path 要避開的,其實相當明確:
- 檔案寫入
- 網路送信
- DB 寫入
- 重的日誌輸出
- 每次
new/malloc/List<T>.Add - 每次字串串接或
ToString() - 重鎖
- 容易在首次存取引起 page fault 的處理
總結:不要把可能變慢的處理帶進 fast path。
特別要注意的是下列 3 項:
-
配置與釋放 fast path 應該先配置緩衝區再重複使用,比較穩。
-
阻塞 I/O 在開發機上看起來夠快,正式環境常常會抖。
-
Page fault 啟動時先摸一次必要的記憶體,就能有差。
必要時也會用到 VirtualLock,但那只是輔助。
首先要把 fast path 本身變輕、並預先準備好必要記憶體,順序在先。
4.6. 優先順序只提高必要執行緒
優先順序的基本原則是 不要全部提高。 先只針對真正對時間敏感的執行緒。
思路大致是:
- UI 與一般 worker 維持普通優先順序
- 只對 fast path 執行緒按需提高
- 儲存、送信、日誌彙整這類後端工作考慮 background mode
- 不先動整個行程,先從執行緒單位思考
音訊、影像這類持續串流,先考慮 MMCSS 會比較自然。 MMCSS 是 Windows 為多媒體準備的排程機制。 比起單純硬調高優先順序,這比較符合 Windows 的做法。
另一方面,REALTIME_PRIORITY_CLASS 不是一開始就上的設定。
有時會有效,但副作用也大;在專用機上充分驗證行為,只在真正必要時才考慮 比較安全。
4.7. 計時器、CPU、電源設定要一起看
在普通 Windows 上,光改程式碼往往還不夠。 等待方式、CPU 的配置、電源設定最好一起看,比較清楚。
要確認的點:
- 經過時間量測有沒有用
QueryPerformanceCounter/Stopwatch - 等待能否使用裝置事件或高精度 waitable timer
- 若用
timeBeginPeriod,是否只在必要期間使用 - 是否根據量測結果決定要不要釘 CPU
- AC 供電、電源模式、process power throttling 是否確認過
關於 CPU,一開始就用 hard affinity 釘住,不如先從 SetThreadIdealProcessor 或 CPU Sets 這類 較寬鬆的指定 開始,通常比較好處理。
釘死反而會減少 OS 的喘息空間。
4.8. 把遲到呈現出來
最後重要的是 不要隱藏遲到的事實。 週期違規與其當例外丟掉,不如計數並記錄,後面比較好追根究柢。
最起碼保有這些:
- 預定開始時刻
- 實際開始時刻
- 實際結束時刻
- lateness
- 執行時間
- missed deadline 次數
- 連續 missed deadline 次數
- queue 深度
- 掉落數
即使這樣仍有大尖峰,就要懷疑應用程式以外的因素。 普通 Windows 可能因 DPC / ISR、驅動、page fault、溫度、時脈變動 而抖。 到這階段,用 ETW / WPR / WPA 或 LatencyMon 分析,比較容易看到原因。
5. 電源設定、OS 設定檢查清單
在普通 Windows 上容易生效的設定,主要落在 Windows 標準範圍內。 比起一開始就進 BIOS / UEFI,依下列順序看會比較可重現。
flowchart LR
a["確認 AC 供電與電源模式"] --> b["必要時建立專用電源計畫"]
b --> c["確認 power throttling 與 timer 相關"]
c --> d["降低背景負載"]
d --> e["有需要時再看 BIOS / UEFI"]
做成檢查清單如下:
-
用 AC 供電 用電池驅動調教,結果不易穩定。
-
確認 [設定] > [系統] > [電源與電池] 的電源模式 正式或量測時,用偏 [最佳效能] 會比較直觀。
-
必要時準備專用電源計畫 日常用平衡,只有正式/量測時切專用計畫,實務上好管理。
-
確認最低處理器狀態 / 最高處理器狀態 在專用機或量測時,AC 情況下試試 100% / 100% 值得一試。 不過熱、耗電、風扇噪音會上升。
-
確認 process power throttling / EcoQoS 若套用了省電方向的設定,可能影響短時間等待或執行速度。
-
減少不必要的背景負載 雲端同步、自動更新、重型瀏覽器、常駐監控、索引建立等照樣會影響。
-
最小化或接近不可見狀態下也測一下 Windows 11 上,不可見的應用程式 timer resolution 行為可能改變。
-
BIOS / UEFI 放最後 C-state 或廠商獨有的靜音/Eco 設定可能有效,但機型依賴高。 先在 Windows 側調完再碰,比較好掌握。
6. 量測與評估
6.1. 要記錄什麼
至少想記錄這些:
- 週期預定時刻
- 實際開始時刻
- 實際結束時刻
- lateness
- 執行時間
- missed deadline 次數
- 連續 missed deadline 次數
- queue 深度
- 掉落數
- DPC / ISR 尖峰
- page fault
- 溫度 / 時脈變動
只看平均,在正式環境裡會困擾的那種延遲很容易被蓋掉。 普通 Windows 上出問題的,常常是 平均很快,但偶爾大幅度延遲 這種形態。
6.2. p99 / p99.9 / max 是什麼
這類用語先理解意義,後面會順很多。
-
平均 看整體大致傾向的指標。 但偶爾冒出的大延遲容易被埋沒。
-
p99 99% 的樣本落在此值以下 的分界。 量了 1000 次,大概可理解為 去掉最慢的 10 次後的上限附近。
-
p99.9 99.9% 的樣本落在此值以下 的分界。 量了 1000 次,大概是 去掉最慢的 1 次後的上限附近。
-
max 表現最差那一次的值。
flowchart LR
sample["收集 1000 次的延遲"] --> sort["由小到大排序"]
sort --> p99["p99:第 990 位附近"]
sort --> p999["p99.9:第 999 位附近"]
sort --> max["max:第 1000 位"]
實務上除了平均,也看 p99 / p99.9 / max,比較貼近體感。 想把「平常還好,偶爾會卡」量化,這種看法很好用。
注意一點:要看 p99.9,樣本數也要夠。 10Hz 的處理,要湊 1000 個樣本就要 100 秒。 樣本太少就看 p99.9,會被偶發的 1 次強烈拉走。
6.3. 用什麼看
工具大致是:
-
應用程式內量測 先自己取 period、lateness、execution time、queue depth
-
ETW / WPR / WPA 看 CPU、context switch、DPC / ISR、page fault
-
LatencyMon 對驅動源頭的抖動做初步定位
-
溫度 / 時脈監控 觀察熱或降頻的影響
先靠應用程式內量測,區分是自己處理重,還是被外部擋住。 確認之後再用 ETW / WPR / WPA 追 DPC / ISR 或 page fault 的影響會更有效率。
6.4. 測試條件
測試只用安靜的開發機不夠。 至少下列條件要分開看:
- 啟動後暖機前
- 暖機後
- 長時間連續運行
- UI 在前景
- UI 最小化 / 近乎不可見
- AC 供電
- 電池驅動
- 有網路或磁碟負載的狀態
不在接近正式的條件下觀察,事後很容易發生「開發機上明明沒事」。
7. 粗略的分類
在普通 Windows 上,現實的目標值大致可如下整理。
-
10~20ms 級,偶爾抖動能吸收 切 fast / slow、固定長度佇列、一般~略高優先順序、事件驅動就足夠。
-
1~5ms 級,要持續跟上 把 fast path 做到免配置、專用執行緒、MMCSS 或謹慎的優先順序調整、高精度 waitable timer、AC 供電、偏最佳效能的電源設定,要一起看。
-
接近 1ms 以下,而且長時間高負載都不能漏 光靠普通 Windows 的 user-mode 應用程式單獨做就吃緊。 要把關鍵部分設計到別處逃生比較好。
-
GUI、日誌、通訊、DB 全部擠在一起 不要一個行程一個迴圈全包,拆開職責比較穩。 後段的情況容易把前段期限弄壞。
8. 總結
在普通 Windows 上追求軟即時時,首先要看的是:
- 週期迴圈是不是在依賴
Sleep - fast path 與 slow path 有沒有切開
- 佇列是不是固定長度,溢位方針有沒有定
- fast path 有沒有混進 I/O、配置、重鎖
- 優先順序或 MMCSS 是不是只用在需要的執行緒
- 普通 Windows 的電源設定與量測有沒有整合到位
在普通 Windows 上,光靠優先順序就穩定的情況其實不多。 把設計、等待方式、電源設定、量測湊齊,才真正成為不易延遲的形狀。
反過來說,按這個順序整理,即使是普通 Windows,也能把 soft real-time 做到相當實用的地步。
9. 參考資料
- Multimedia Class Scheduler Service
- AvSetMmThreadCharacteristicsW function
- SetThreadPriority function
- SetPriorityClass function
- timeBeginPeriod function
- CreateWaitableTimerExW function
- Acquiring high-resolution time stamps
- GetSystemTimePreciseAsFileTime function
- SetProcessInformation function
- VirtualLock function
- CPU Sets
- SetThreadIdealProcessor function
- SetThreadAffinityMask function
- Processor power management options
- 變更 Windows PC 的電源模式
- CPU 的分析 (WPA / WPT)
- Stopwatch Class
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
Windows 上為什麼應先用事件等待而不是計時器等待 - 避免以約 15.6ms 粒度做輪詢
本文聚焦於 Windows 上短時間 timed wait 為何不可靠,並說明在工作抵達、I/O 完成或停止請求等場景應改採 event 驅動。讀者可學會以 system clock 粒度與排程延遲為線索,挑選 event、semaphore、WaitOnAddress 或...
發生非預期例外時的 checklist - 要讓應用結束還是繼續,先看的判斷表
本文以 C# / .NET 與 Windows 應用為前提,把非預期例外發生後該結束還是繼續的判斷拆成失敗單位、共用狀態、外部副作用、原生邊界四個觀察點,並提供判斷表與典型情境,協助讀者在 catch 之前先判斷是否還能信任應用狀態。
Windows 應用程式開發中遵守最低限度安全性的檢核表
用檢核表形式整理 WPF / WinForms / WinUI / C++ / C# 等 Windows 應用程式發佈前最低限不想漏的安全性要點。涵蓋避免不必要的管理員權限、EXE 與更新物簽章加時間戳、改用 DPAPI 與 Credential Locker、保留 HTT...
把 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 開發、故障調查與既有資產活用文章的主題中心。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
技術諮詢 & 設計審查
協助釐清設計方向、架構邊界、生命週期責任,以及既有 Windows 資產的處理方式。