在普通 Windows 上尽量做到软实时的实践指南
· 小村 豪 · Windows开发, 软实时, 设计, 测量
在 Windows 上开发周期处理、音频处理、视频处理、测量、设备控制这类「一旦延迟就麻烦」的处理时,很容易给人一种「Windows 恐怕吃不消」的印象。这种印象一半对,一半不对:Windows 确实不是 hard real-time 操作系统,但只要把 设计、实现、测量、运维 这几个环节都做扎实,就能把它作为 soft real-time 用到相当实用的程度。
本文讨论的是 不依赖特殊 RTOS 扩展、自研内核驱动或专用控制器的普通 Windows 10 / 11。内容偏向实务:在日常使用的桌面 / 笔记本电脑上的 user-mode 应用中,延迟与抖动究竟能压到什么程度。音频、视频、周期控制、数据采集这些场景在细节上各有不同,但容易出问题的地方相当相似,因此本文把这些共同点整理成了一份 检查清单。
目录
- 先说结论(一句话)
- 普通 Windows 上的「软实时」是什么
- 2.1. 本文所说的「普通 Windows」
- 2.2. 能做到什么,从哪里开始变难
- 2.3. 先说明一下术语
- 延迟与抖动的主要原因
- 3.1. 调度器与优先级
- 3.2. DPC / ISR 与驱动程序
- 3.3. 页面错误与内存
- 3.4. 定时器分辨率与电源管理
- 3.5. 核心迁移与发热
- 在普通 Windows 上减少延迟的实践检查清单
- 4.1. 周期循环与等待方式
- 4.2. fast path / slow path 与固定长度队列
- 4.3. 优先级 / MMCSS / background mode
- 4.4. 内存 / GC / 首次开销
- 4.5. 电源设置 / EcoQoS / timer resolution
- 4.6. CPU 分配 / 核心迁移 / 发热
- 4.7. 驱动 / DPC / ISR / 外部干扰的排查
- 测量与评估
- 5.1. 应该记录什么
- 5.2. p99 / p99.9 / max 的解读方式
- 5.3. 用什么工具查看
- 5.4. 测试的方法
- 大致的选型建议
- 总结
- 参考资料
1. 先说结论(一句话)
- 在普通 Windows 上追求的目标不是 hard real-time 的保证,而是作为 soft real-time 做到「不容易延迟、即使延迟也不容易崩」的结构。
- 效果最大的做法是 把热路径(hot path)做短、做成固定长度、并且非阻塞。
- 把 fast path(采集 / 控制)与 slow path(保存 / 通信 / UI)分开,中间用 固定长度队列 连接。
- 周期循环不依赖
Sleep(1),而是按 绝对期限 运行。 - 对于音频、视频这类连续流,首先考虑使用 MMCSS。
- 时间测量使用 QueryPerformanceCounter(QPC),在 .NET 中则使用
Stopwatch。 - 等待优先使用 设备事件 或 高精度 waitable timer(可等待计时器)。
timeBeginPeriod只在需要的时间段内使用,不要以「始终生效」为前提进行设计。- 在实际运行中,AC 供电 / 电源模式 / EcoQoS 的处理 / 后台负载的整理 都很有效。
- 评估时不能只看平均值,还要看 p99(测量 100 次时,最慢的那 1 次开始显现的分界线) / p99.9 / max / miss 次数 / DPC / ISR / page fault / 队列深度。
总而言之,在普通 Windows 上,比起提高优先级,通过设计来减少延迟发生的原因 更为有效。 优先级和电源设置固然重要,但仅靠它们并不能构建出稳定性。
2. 普通 Windows 上的「软实时」是什么
2.1. 本文所说的「普通 Windows」
这里所说的 普通 Windows,大致以下面这些为前提。
- Windows 10 / 11 上常见的桌面 / 笔记本电脑
- 不使用自研的 RTOS 扩展
- 不开发自研的内核模式驱动
- 普通的 user-mode 应用
- 通过常规的 Windows API 与设置来调优
也就是说,本文谈的不是「把一整台专用机打造成实时控制设备」,而是「在普通 Windows PC 上究竟能把延迟压到什么程度才现实」。
flowchart LR
A["普通的 Windows 10 / 11 PC"] --> B["user-mode 应用"]
B --> C["以 soft real-time 为目标"]
C --> D["降低延迟"]
C --> E["减小抖动"]
C --> F["观测 deadline miss,防止系统崩溃"]
G["想保证零期限违反"] -.-> H["RTOS / 专用控制器 / FPGA / 设备端处理"]
2.2. 能做到什么,从哪里开始变难
即便是普通 Windows,如果是下面这类处理,也能相当现实地做到「不容易延迟」的状态。
- 几毫秒到几十毫秒级别的周期处理
- 音频 / 视频的缓冲驱动
- 传感器采集与控制回路
- 类似软 PLC 的固定周期处理
- 在与 UI 分离的独立线程中运行的低延迟管线
不过,这里所说的「能做到」并不意味着 能把偶发的延迟尖峰完全归零,实际追求的始终是下面这种状态。
- 让平时的延迟保持较低
- 让抖动保持较小
- 偶尔错过期限也不会导致系统崩溃
- 能够观测到「错过期限」这一事实
反过来,如果出现下面这类需求,仅靠普通 Windows 的 user-mode 就很难满足了。
- 想保证零期限违反
- 想长时间稳定地守住数百微秒以下的延迟
- 想与繁重的 GUI、网络、存储共存
- 想在电池供电或省电优先模式下依然维持
- 不允许出现由驱动或设备引起的尖峰
遇到这类情况,比较安全的做法是 只把真正对时间要求严苛的部分交给设备端固件、专用控制器、FPGA 或 RTOS 处理。
2.3. 先说明一下术语
先把本文中出现的术语含义梳理一下。
| 术语 | 一句话概括 | 实务角度 |
|---|---|---|
| soft real-time | 允许偶尔延迟,但要把延迟控制得小,且延迟后不会导致崩溃的理念 | 普通 Windows 首先要追求的就是这个 |
| hard real-time | 要保证零期限违反的世界 | 不是普通 Windows 的 user-mode 单独能够追求的目标 |
| 抖动(jitter) | 周期或响应时间的波动 | 即使平均值不错,抖动大的话实际运行中也会不稳定 |
| deadline miss | 处理未能在预定时刻前完成 | 不要隐瞒,要计数并记录到日志中 |
| p99 / p99.9 | 用来观察「慢」这一端尾部的指标 | p99 是「测量 100 次中,最慢的那 1 次开始显现的分界线」 |
| DPC / ISR | 驱动或中断相关的内核侧处理 | 如果耗时较长,user-mode 线程就会被阻塞等待 |
| MMCSS | Windows 用于给音频/视频等对时间敏感的处理分配 CPU 的机制 | 对不希望缓冲区中断的处理很有效 |
| QPC | 指 QueryPerformanceCounter | 测量经过时间的基础手段,是高精度计数器,而不是挂钟时间 |
3. 延迟与抖动的主要原因
普通 Windows 上周期处理出现延迟的原因,基本都可以归结到下图中的某一项。
flowchart TD
Late["周期处理出现延迟"] --> S["调度器 / 优先级"]
Late --> D["DPC / ISR / 驱动"]
Late --> M["页面错误 / 内存"]
Late --> T["定时器分辨率 / 电源管理"]
Late --> C["核心迁移 / 发热"]
3.1. 调度器与优先级
Windows 中线程的执行顺序由优先级决定。 优先级相同时按轮转(round-robin)方式运行,一旦优先级更高的线程变为可执行状态,优先级较低的线程就会被抢占。
也就是说,即使认真编写周期线程,
- 其他线程
- 其他进程
- 操作系统内部处理
- 安全软件
- 设备辅助处理
- 后台同步
先于它运行的情况也很常见。
3.2. DPC / ISR 与驱动程序
这一点相当重要。 即使把应用侧的优先级调整好,只要 DPC(Deferred Procedure Call,延迟过程调用)或 ISR(Interrupt Service Routine,中断服务例程) 执行时间较长,在这期间 user-mode 线程就无法运行。
容易成为原因的,大多是下面这些设备或驱动。
- USB
- Wi-Fi / Bluetooth
- 存储
- 音频
- GPU
- ACPI / 电源相关
即使应用代码本身没有问题,也可能因为驱动或硬件方面的原因被阻塞。 如果想着「把应用的优先级再调高一点应该就能赢」,通常都会吃亏。
3.3. 页面错误与内存
如果在热路径中发生 page fault(所需页面不在内存中,需要去取回),延迟会一下子变大。
下面列出特别要避免的模式。
- 首次访问时的页面提交
- 延迟加载
- 内存映射文件的换入(page-in)
- 超出必要的动态分配
- 大对象或碎片化的堆
在周期处理的主体部分,做到 提前分配好所需内存,并在启动时先访问一次 就比较合适了。
3.4. 定时器分辨率与电源管理
「因为想每 1ms 运行一次,所以用 Sleep(1)」这种想法,大多数情况下都行不通。
Windows 的等待精度会受到定时器分辨率、调度和电源状态的影响。
此外,提高定时器分辨率的设置 在改善等待精度的同时,也会对功耗以及系统整体行为产生副作用,这一点也不能忽视。
3.5. 核心迁移与发热
线程在核心之间迁移时,会发生缓存重新预热的情况。 这件事本身 OS 大多能妥善处理,但在负载较高的环境中会成为波动的原因。
此外,长时间运行时发热也不能忽视。 一旦触发温度限频(thermal throttling),原本稳定的周期就可能被打乱。
4. 在普通 Windows 上减少延迟的实践检查清单
从这里开始是实务部分。 针对上一节中提到的各种原因,本节以检查清单的形式,整理在普通 Windows 上 应该确认什么、应该避免什么、应该提前决定什么。
4.1. 周期循环与等待方式
首先,典型的反面模式是这样的。
while (running)
{
Sleep(1);
Step();
}
这并不是「1ms 周期」,而是一个 先等待 1ms 以上,再在此基础上加上 Step() 的执行时间 的循环。
而且等待的超量部分会原样不断累积。
flowchart LR
subgraph Bad["基于相对时间"]
B1["Sleep(1)"] --> B2["Step()"]
B2 --> B1
end
B2 --> B3["等待误差与执行时间逐渐累积"]
subgraph Good["基于绝对期限"]
G1["next += period"] --> G2["WaitUntil(next - margin)"]
G2 --> G3["必要时进行短暂 spin"]
G3 --> G4["FastStep()"]
G4 --> G1
end
G4 --> G5["不容易积累漂移(drift)"]
检查清单
- 没有把
Sleep(1)当作周期循环的基础 - 周期按照
next += period的 绝对期限 运行 - 等待优先使用 设备事件 或 waitable timer(可等待计时器)
- 只在最后的微调阶段使用极短的 busy-spin(空转等待)
timeBeginPeriod只在需要的时间段内使用,用完后还原- 已确认在最小化 / 隐藏 / 不可见状态下的行为
周期循环按照 绝对期限 而不是相对时间来运行,会更稳定。
int64_t next = QpcNow() + periodTicks;
while (running)
{
WaitUntil(next - wakeMarginTicks);
while (QpcNow() < next)
{
CpuRelax(); // 最后只做短暂 spin
}
int64_t started = QpcNow();
FastStep();
int64_t finished = QpcNow();
RecordTiming(next, started, finished);
next += periodTicks;
while (finished > next)
{
++missedDeadlines;
next += periodTicks;
}
}
4.2. fast path / slow path 与固定长度队列
构建的基本原则是:fast path 中只放「对期限敏感的工作」,其他一切都交给 slow path。
flowchart LR
Input["设备 / 采集事件"] --> Fast["fast path:采集・控制・最小限度的复制"]
Fast --> Queue["固定长度队列"]
Queue --> Slow["slow path:保存・发送・UI・汇总"]
Fast --> Metrics["记录 lateness / miss / 队列深度"]
Metrics --> Slow
fast path 中要做的事,收窄到这个程度就够了。
- 数据采集
- 控制值计算
- 必要最小限度的复制
- 时间戳记录
- 写入队列
- 记录 miss / overrun
其他一切都放到 slow path 中处理。
检查清单
- 热路径中没有进行文件写入、网络发送、数据库写入
- 热路径中没有输出大量日志、调用
Flush、执行同步 RPC - 已按线程或职责明确划分 fast path / slow path
- 队列使用 固定长度
- 已提前决定队列溢出时的应对方针
- 已监测 miss 次数、drop 数量、队列深度
- UI 更新与日志汇总已分离到较低的周期中处理
当队列变满时,提前明确应对方针会更安全,不要让它含糊不清。
flowchart TD
Overflow["队列已满"] --> Policy{"要保住什么?"}
Policy -->|最新值更重要| Latest["丢弃旧元素,保留最新值"]
Policy -->|全部数据都重要| All["告警 / 停止 / 上游限流"]
Policy -->|日志用途| Log["丢弃旧元素,只记录 drop 数量"]
4.3. 优先级 / MMCSS / background mode
优先级的基本原则是 不要把所有线程都提高。 在普通 Windows 上,「只提高重要线程的优先级,同时切实降低后台工作的优先级」这种做法效果更好。 background mode 是一种不仅针对 CPU,也把 I/O 等资源一并调低优先级处理的机制。
flowchart TD
Work["划分工作"] --> Critical["对期限敏感的线程"]
Work --> Worker["保存 / 发送 / 压缩 / 汇总"]
Work --> UI["UI"]
Critical --> P1["必要时使用较高优先级或 MMCSS"]
Worker --> P2["background mode / 较低优先级"]
UI --> P3["普通优先级"]
P1 --> Warn["一开始不要直接设为 REALTIME_PRIORITY_CLASS"]
检查清单
- 没有把所有线程都设为高优先级
- 只提高了真正对时间要求严苛的线程的优先级
- 保存、发送、压缩、同步等后台工作已降到 background mode
- 对音频、视频、采集、播放等连续缓冲处理已考虑使用 MMCSS
- 优先按 线程单位 而不是整个进程来考虑
- 在明确有必要之前,不使用
REALTIME_PRIORITY_CLASS
MMCSS(Multimedia Class Scheduler Service)对于 音频 / 视频这类「希望在一定时间内填满缓冲区」的处理 特别有效。 比起单纯让高优先级线程一直运行,这种方式更符合 Windows 的设计思路。
代码大致是这样的感觉。
DWORD taskIndex = 0;
HANDLE avrt = AvSetMmThreadCharacteristicsW(L"Pro Audio", &taskIndex);
if (!avrt)
{
throw std::runtime_error("AvSetMmThreadCharacteristicsW failed");
}
// 运行对时间敏感的循环
if (!AvRevertMmThreadCharacteristics(avrt))
{
throw std::runtime_error("AvRevertMmThreadCharacteristics failed");
}
4.4. 内存 / GC / 首次开销
如果在热路径中每次都调用 new / malloc / List<T>.Add / 字符串拼接 / LINQ,迟早会暴露出回收或重新分配相关的问题。GC(垃圾回收)本身并不是坏事,但 分配次数多的代码,其影响会以抖动的形式表现出来。
flowchart LR
Start["启动"] --> Alloc["分配所需缓冲区"]
Alloc --> Touch["先访问一次,预热页面"]
Touch --> Warm["完成 JIT / DLL 加载 / 首次 I/O"]
Warm --> Measure["之后再进行正式测量 / 正式运行"]
检查清单
- 热路径中没有每次都进行内存分配 / 释放
- 已在启动时预先分配好所需缓冲区
- 已在启动时先访问一次以预热页面
- 首次 JIT、首次 DLL 加载、首次 I/O 没有混入正式测量中
- 循环过程中没有让巨大结构体或可变长日志不断增长
- 即使使用了
VirtualLock,也仅限于极小的关键区域
.NET 侧的检查
- 时间测量使用
Stopwatch/Stopwatch.GetTimestamp() - 热路径中没有使用 LINQ、字符串拼接、
ToString()、生成大量日志 - 没有把
async/await带入热路径 - 已将预热前与预热后分开评估
4.5. 电源设置 / EcoQoS / timer resolution
这一部分看起来不起眼,但确实有效。 即使把代码打磨得很好,如果上层的电源控制影响较强,结果依然不会稳定。
flowchart TD
Power["普通 Windows 的电源相关设置"] --> AC["使用 AC 供电运行"]
Power --> Mode["电源模式:偏向最佳性能"]
Power --> Plan["必要时使用面向正式环境的专用电源计划"]
Power --> QoS["对时间敏感的进程要避免 EcoQoS"]
Power --> Timer["确认定时器分辨率请求的处理方式"]
检查清单
- 正式评估首先在 AC 供电 下进行
- 已将
[设置] > [系统] > [电源和电池] > [电源模式]调整为偏向 最佳性能 - 运行期间没有使用省电模式 / 省电优先模式
- 已检查厂商自带工具中的静音 / eco / 电池优先模式
- 没有不慎把对时间敏感的进程设为 EcoQoS(偏省电的 QoS)
- 没有在对时间敏感的进程侧启用
IGNORE_TIMER_RESOLUTION - 已确认最小化 / 隐藏时定时器分辨率请求的效果是否会发生变化
- 已把日常使用与正式环境 / 测量 / 演示用的电源设置区分开
timeBeginPeriod 如果使用得当会很有帮助,但 并不是万能药。
- 在真正需要之前才调用
- 使用结束后要通过
timeEndPeriod还原 - Windows 10 version 2004 以后,其行为已不再是过去那种完全全局的效果
- 在 Windows 11 上,如果拥有窗口的进程完全隐藏 / 被最小化 / 不可见 / 无声音,高分辨率可能无法得到保证
- 即使提高了分辨率,也不代表 QPC 的精度会提升
如果怀疑受到电源或 QoS 的影响,可以用 SetProcessInformation 确认 power throttling 的状态。
PROCESS_POWER_THROTTLING_STATE state{};
state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
state.ControlMask =
PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION;
state.StateMask = 0; // HighQoS(偏向性能优先) + 尊重定时器分辨率请求
if (!SetProcessInformation(
GetCurrentProcess(),
ProcessPowerThrottling,
&state,
sizeof(state)))
{
throw std::runtime_error("SetProcessInformation failed");
}
4.6. CPU 分配 / 核心迁移 / 发热
CPU 分配方面,比起一开始就 固定到特定核心(hard affinity / CPU pinning),从 「尽量在这一组核心上运行」这种接近 soft affinity 的设定 开始,往往效果更好。
flowchart LR
Measure["先测量"] --> Ideal["SetThreadIdealProcessor / CPU Sets"]
Ideal --> Check{"改善是否足够?"}
Check -->|是| Keep["就此止步"]
Check -->|否| Hard["最后再考虑 SetThreadAffinityMask"]
Measure --> Therm["同时确认温度 / 主频 / 长时间运行的情况"]
检查清单
- 先测量,之后才调整 CPU 分配
- 没有一开始就固定到特定核心
- 首先尝试了
SetThreadIdealProcessor或 CPU Sets - 把
SetThreadAffinityMask当作最后的手段 - 已在长时间运行中确认温度、主频、温度限频情况
- 已确认笔记本电脑的静音模式或低噪音模式
按下面的顺序来做比较安全。
- 先进行测量
- 必要时使用 ideal processor / CPU Sets
- 如果仍需要进一步改善,再考虑固定到特定核心
固定到特定核心看起来会有效果,但因为这会 减少操作系统的调度余地,如果轻易使用,反而可能变得不够灵活。
4.7. 驱动 / DPC / ISR / 外部干扰的排查
遇到「偶尔 max 值暴涨」「平均值不错,但 p99.9 很差」这类情况时, 最好也怀疑一下应用代码之外的外部干扰因素。
flowchart TD
Spike["出现了 late / miss / max spike"] --> Q1{"自己的处理时间也很长吗?"}
Q1 -->|是| App["缩短热路径 / 减少分配 / 去除 I/O"]
Q1 -->|否| Q2{"存在 DPC / ISR 尖峰吗?"}
Q2 -->|是| Driver["检查 USB / Wi-Fi / Bluetooth / GPU / Audio / Storage / ACPI / 驱动更新"]
Q2 -->|否| Q3{"存在 page fault / GC / 首次开销吗?"}
Q3 -->|是| Mem["预先分配 / 预热 / 减少堆负载"]
Q3 -->|否| Q4{"存在电池 / 省电 / 发热的影响吗?"}
Q4 -->|是| Pow["AC 供电 / 电源设置 / 散热 / 长时间测试"]
Q4 -->|否| ETW["用 ETW / WPA / LatencyMon 深入排查"]
检查清单
- 已检查 Wi-Fi / Bluetooth / USB / 存储 / GPU / 音频相关的驱动
- 已停止不必要的云同步、建立索引、自动更新,并进行对比
- 已尝试最小化后是否会变差、关闭屏幕后是否会变差
- 已用 LatencyMon 或 ETW 查看 DPC / ISR 的趋势
- 已把「自己的处理太重」与「被外部因素阻塞」区分开来看
5. 测量与评估
5.1. 应该记录什么
至少应该采集下面这些信息。
- 周期预定时刻
- 实际开始时刻
- 实际结束时刻
- lateness(相对预定开始时刻延迟了多久才开始)
- 执行时间
- missed deadline 次数
- 连续 missed deadline 次数
- 队列深度(queue depth)
- drop 数量
- CPU 使用率
- 各核心的负载偏差
- DPC / ISR 尖峰
- page fault
- 温度 / 主频变化
只看平均值很难把握本质。 在实际运行中真正让人头疼的,是偶尔出现的大幅延迟尖峰。
5.2. p99 / p99.9 / max 的解读方式
p99 之类的指标,是 用来观察慢的那一端的尾部 的。 只看平均值,偶尔出现的大幅延迟就会被掩盖。
| 指标 | 含义 | 测量 10,000 次时的直观感受 |
|---|---|---|
| 平均 | 整体的平滑值 | 尖峰容易被掩盖 |
| p50 | 中间值 | 接近日常的实际体感 |
| p95 | 最慢 5% 开始显现的分界线 | 排除最慢 500 次后的边界 |
| p99 | 最慢 1% 开始显现的分界线 | 排除最慢 100 次后的边界 |
| p99.9 | 最慢 0.1% 开始显现的分界线 | 排除最慢 10 次后的边界 |
| max | 最差值 | 最慢的那 1 次 |
比如,
- 平均:0.8ms
- p99:1.2ms
- p99.9:3.5ms
- max:28ms
这种情况就说明 平时速度很快,但偶尔会出现较大的尖峰。 在普通 Windows 上,真正的问题大多就出现在这段 从 p99 到 max 的尾部。
5.3. 用什么工具查看
常用的工具大致是固定的。
- 应用内测量
首先自行采集
period / lateness / execution time / queue depth / drop - ETW / WPR / WPA 深入分析 CPU、context switch、DPC / ISR、page fault
- LatencyMon 排查由驱动引起的波动
- 温度 / 主频监控 查看发热带来的影响
flowchart LR
App["应用内测量"] --> Dist["p50 / p95 / p99 / p99.9 / max"]
App --> Miss["miss / drop / 队列深度"]
ETW["ETW / WPR / WPA"] --> Root["context switch / DPC / ISR / page fault"]
Temp["温度 / 主频监控"] --> Root
Dist --> Decide["确定改善的优先顺序"]
Miss --> Decide
Root --> Decide
用到 WPA 这一步会稍微费些功夫, 但对于区分 是 DPC / ISR 引起的,还是单纯自己的处理太重,相当有效。
5.4. 测试的方法
光靠安静的测试环境是不够的。至少应该把下面这些条件区分开来分别测试。
- 启动后、预热之前
- 预热之后
- 长时间连续运行
- UI 处于前台
- UI 最小化 / 接近隐藏的状态
- AC 供电
- 电池供电
- 网络或磁盘处于高负载的状态
只靠测试环境进行评估,容易漏掉实际运行中才会出现的问题。 普通 Windows 的行为很容易被「使用方式」牵着走,因此把测试条件贴近实际使用场景来确认非常重要。
6. 大致的选型建议
-
10~20ms 级别,能够容忍偶尔的波动 → fast / slow 分离、固定长度队列、普通优先级到略高优先级、事件驱动,很多情况下已经足够
-
1~5ms 级别,希望持续按时完成 → 热路径无分配化、专用线程、MMCSS 或谨慎的优先级调整、高精度 waitable timer(可等待计时器)、AC 供电、偏向最佳性能的电源设置
-
逼近 1ms 以下,而且长时间 / 高负载下也不想错过期限 → 仅靠普通 Windows 的 user-mode 会相当吃力,应考虑把关键部分移交到其他位置处理
-
GUI / 日志 / 通信 / 数据库全部混在一起 → 不要用「全部塞进 1 个进程 1 个循环」的方式硬扛,而要分离职责;因为后段的情况很容易破坏前段的期限
7. 总结
有两个前提需要牢记。
- 在普通 Windows 上追求的目标不是 hard real-time 的保证,而是作为 soft real-time 把延迟与抖动降到最小,并让系统即使出现期限违反也不会崩溃
- 效果最大的做法,是整理热路径,而不是调整优先级
在实现层面,下面这些做法比较有效。
- 分离 fast path / slow path
- 使用固定长度队列,并提前决定溢出时的应对方针
- 用 QPC 测量,用 event / waitable timer(可等待计时器)等待
- 热路径中避免内存分配、阻塞 I/O、重量级锁
在运维层面,下面这些做法比较有效。
- 使用 AC 供电运行
- 单独设置面向正式环境的电源方案
- 减少不必要的后台负载
- 用 p99 / p99.9 / max 与 miss 次数来评估
普通 Windows 上的软实时,并不是仅靠优先级设置就能决定的。只要把设计、实现、电源设置、测量和运维分别做扎实,就能构建出相当稳定的系统。
8. 参考资料
- Multimedia Class Scheduler Service
- AvSetMmThreadCharacteristicsW function
- SetThreadPriority function
- SetPriorityClass function
- timeBeginPeriod function
- CreateWaitableTimerExW function
- Acquiring high-resolution time stamps
- QueryPerformanceCounter function
- GetSystemTimePreciseAsFileTime function
- SetProcessInformation function
- VirtualLock function
- CPU Sets
- SetThreadIdealProcessor function
- SetThreadAffinityMask function
- Processor power management options
- Change the power mode for your Windows PC
- Power settings in Windows 11
- CPU Analysis (WPA / WPT)
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
为什么在 Windows 上应优先选择事件等待而非 Sleep(1)
在 Windows 上,短时 timed wait 的精度会受到 system clock 粒度和调度延迟的影响。本文整理了为什么在等待任务到达、I/O 完成或停止请求时,应该采用事件驱动而不是按固定间隔轮询。
在桌面应用中使用 .NET Generic Host 与 BackgroundService 的理由
本文整理在 Windows 工具或常驻应用中,如何使用 Generic Host 与 BackgroundService 来梳理启动、定期处理、退出处理、日志、配置与 DI。
FileSystemWatcher 实务指南:应对遗漏通知与重复通知
本文从遗漏通知、重复通知、完成判定的陷阱、重新扫描、原子式 claim、idempotency(幂等性)等角度,整理 FileSystemWatcher 的使用方法与注意事项。
用 PerfView 与 dotnet-trace 定位“变慢”的原因 ── .NET 性能排查实务入门
当业务应用出现“变慢”“CPU 占满”“偶尔卡死”时,该用哪个工具看什么?本文梳理 PerfView 与 dotnet-trace 的分工、CPU 采样的解读方法(inclusive/exclusive)、用 ThreadTime 排查阻塞时间,以及与 EventSourc...
Windows 事件日志・ETW 入门 ── 让业务应用程序的日志接入操作系统标准机制
Windows 业务应用程序的日志,是不是只靠文件日志来解决?事件日志与 ETW,是运维人员和操作系统标准工具所能看到的另一层记录。本文整理三种手段的分工方式、.NET 中的写入方法、通过 EventSource 进行的 ETW 埋点、收集与排查的实务做法,以及来源未注册、...
常见问题
汇总了咨询这一主题时常见的问题。
- 在 Windows 上能实现实时处理吗?
- 无法保证 hard real-time(零期限违反),但只要把设计、实现、测量、运维这几个环节都做到位,就可以把 soft real-time 做到相当实用的程度。几毫秒到几十毫秒级别的周期处理、音频/视频的缓冲驱动、传感器采集与控制回路等,在普通的 Windows 10/11 上都是现实可行的。反过来,如果需要保证零期限违反,或者需要长时间稳定守住数百微秒以下的延迟,就应该考虑把这部分交给 RTOS、专用控制器、FPGA 或设备端处理。
- 为什么周期处理不应该使用 Sleep(1)?
- Sleep(1) 并不是「按 1ms 周期」运行,而是「至少等待 1ms 以上,再加上处理本身耗费的时间」,等待的超量部分会不断累积。周期循环应该按照 next += period 这种绝对期限来运行,等待优先使用设备事件或高精度的 waitable timer,只在最后做极短的忙等(busy-spin)微调才比较稳定。timeBeginPeriod 也只应在需要的时间段内使用,用完后要还原。
- 要减少延迟和抖动,最有效的方法是什么?
- 比起提高优先级,更有效的是把热路径(hot path)做得更短、长度固定、并且非阻塞。把 fast path(采集、控制)与 slow path(保存、通信、UI)分开,中间用固定长度队列连接,热路径中要避免写文件、发送网络请求、输出大量日志以及每次都重新分配内存。在运维层面,使用 AC 供电、重新检查电源模式、确认 EcoQoS 设置、清理后台负载等做法也很有效。
- 应该用什么指标来评估周期处理的稳定性?
- 不能只看平均值,还要看 p99、p99.9、max 以及错过期限(missed deadline)的次数。比如平均只有 0.8ms,但 max 却达到 28ms,就说明大多数时候很快,但偶尔会出现较大的尖峰;在普通 Windows 上,真正的问题往往就出现在从 p99 到 max 这段「尾部」。同时也应该记录 DPC/ISR 尖峰、page fault、队列深度和温度/主频变化,并把预热前后、长时间运行、最小化状态、电池供电等条件分开评估。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。