在普通 Windows 上尽量做到软实时的实践指南

· · Windows开发, 软实时, 设计, 测量

在 Windows 上开发周期处理、音频处理、视频处理、测量、设备控制这类「一旦延迟就麻烦」的处理时,很容易给人一种「Windows 恐怕吃不消」的印象。这种印象一半对,一半不对:Windows 确实不是 hard real-time 操作系统,但只要把 设计、实现、测量、运维 这几个环节都做扎实,就能把它作为 soft real-time 用到相当实用的程度。

本文讨论的是 不依赖特殊 RTOS 扩展、自研内核驱动或专用控制器的普通 Windows 10 / 11。内容偏向实务:在日常使用的桌面 / 笔记本电脑上的 user-mode 应用中,延迟与抖动究竟能压到什么程度。音频、视频、周期控制、数据采集这些场景在细节上各有不同,但容易出问题的地方相当相似,因此本文把这些共同点整理成了一份 检查清单

目录

  1. 先说结论(一句话)
  2. 普通 Windows 上的「软实时」是什么
    • 2.1. 本文所说的「普通 Windows」
    • 2.2. 能做到什么,从哪里开始变难
    • 2.3. 先说明一下术语
  3. 延迟与抖动的主要原因
    • 3.1. 调度器与优先级
    • 3.2. DPC / ISR 与驱动程序
    • 3.3. 页面错误与内存
    • 3.4. 定时器分辨率与电源管理
    • 3.5. 核心迁移与发热
  4. 在普通 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. 测量与评估
    • 5.1. 应该记录什么
    • 5.2. p99 / p99.9 / max 的解读方式
    • 5.3. 用什么工具查看
    • 5.4. 测试的方法
  6. 大致的选型建议
  7. 总结
  8. 参考资料

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 上究竟能把延迟压到什么程度才现实」。

普通的 Windows 10 / 11 PCuser-mode 应用以 soft real-time 为目标降低延迟减小抖动观测 deadline miss,防止系统崩溃想保证零期限违反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 上周期处理出现延迟的原因,基本都可以归结到下图中的某一项。

周期处理出现延迟调度器 / 优先级DPC / ISR / 驱动页面错误 / 内存定时器分辨率 / 电源管理核心迁移 / 发热

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() 的执行时间 的循环。 而且等待的超量部分会原样不断累积。

基于绝对期限基于相对时间WaitUntil(next - margin)next += period必要时进行短暂 spinFastStep()Step()Sleep(1)等待误差与执行时间逐渐累积不容易积累漂移(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

设备 / 采集事件fast path:采集・控制・最小限度的复制固定长度队列slow path:保存・发送・UI・汇总记录 lateness / miss / 队列深度

fast path 中要做的事,收窄到这个程度就够了。

  • 数据采集
  • 控制值计算
  • 必要最小限度的复制
  • 时间戳记录
  • 写入队列
  • 记录 miss / overrun

其他一切都放到 slow path 中处理。

检查清单

  • 热路径中没有进行文件写入、网络发送、数据库写入
  • 热路径中没有输出大量日志、调用 Flush、执行同步 RPC
  • 已按线程或职责明确划分 fast path / slow path
  • 队列使用 固定长度
  • 已提前决定队列溢出时的应对方针
  • 已监测 miss 次数、drop 数量、队列深度
  • UI 更新与日志汇总已分离到较低的周期中处理

当队列变满时,提前明确应对方针会更安全,不要让它含糊不清。

最新值更重要全部数据都重要日志用途队列已满要保住什么?丢弃旧元素,保留最新值告警 / 停止 / 上游限流丢弃旧元素,只记录 drop 数量

4.3. 优先级 / MMCSS / background mode

优先级的基本原则是 不要把所有线程都提高。 在普通 Windows 上,「只提高重要线程的优先级,同时切实降低后台工作的优先级」这种做法效果更好。 background mode 是一种不仅针对 CPU,也把 I/O 等资源一并调低优先级处理的机制。

划分工作对期限敏感的线程保存 / 发送 / 压缩 / 汇总UI必要时使用较高优先级或 MMCSSbackground mode / 较低优先级普通优先级一开始不要直接设为 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(垃圾回收)本身并不是坏事,但 分配次数多的代码,其影响会以抖动的形式表现出来。

启动分配所需缓冲区先访问一次,预热页面完成 JIT / DLL 加载 / 首次 I/O之后再进行正式测量 / 正式运行

检查清单

  • 热路径中没有每次都进行内存分配 / 释放
  • 已在启动时预先分配好所需缓冲区
  • 已在启动时先访问一次以预热页面
  • 首次 JIT、首次 DLL 加载、首次 I/O 没有混入正式测量中
  • 循环过程中没有让巨大结构体或可变长日志不断增长
  • 即使使用了 VirtualLock,也仅限于极小的关键区域

.NET 侧的检查

  • 时间测量使用 Stopwatch / Stopwatch.GetTimestamp()
  • 热路径中没有使用 LINQ、字符串拼接、ToString()、生成大量日志
  • 没有把 async/await 带入热路径
  • 已将预热前与预热后分开评估

4.5. 电源设置 / EcoQoS / timer resolution

这一部分看起来不起眼,但确实有效。 即使把代码打磨得很好,如果上层的电源控制影响较强,结果依然不会稳定。

普通 Windows 的电源相关设置使用 AC 供电运行电源模式:偏向最佳性能必要时使用面向正式环境的专用电源计划对时间敏感的进程要避免 EcoQoS确认定时器分辨率请求的处理方式

检查清单

  • 正式评估首先在 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 的设定 开始,往往效果更好。

先测量SetThreadIdealProcessor / CPU Sets改善是否足够?就此止步最后再考虑 SetThreadAffinityMask同时确认温度 / 主频 / 长时间运行的情况

检查清单

  • 先测量,之后才调整 CPU 分配
  • 没有一开始就固定到特定核心
  • 首先尝试了 SetThreadIdealProcessor 或 CPU Sets
  • SetThreadAffinityMask 当作最后的手段
  • 已在长时间运行中确认温度、主频、温度限频情况
  • 已确认笔记本电脑的静音模式或低噪音模式

按下面的顺序来做比较安全。

  1. 先进行测量
  2. 必要时使用 ideal processor / CPU Sets
  3. 如果仍需要进一步改善,再考虑固定到特定核心

固定到特定核心看起来会有效果,但因为这会 减少操作系统的调度余地,如果轻易使用,反而可能变得不够灵活。

4.7. 驱动 / DPC / ISR / 外部干扰的排查

遇到「偶尔 max 值暴涨」「平均值不错,但 p99.9 很差」这类情况时, 最好也怀疑一下应用代码之外的外部干扰因素。

出现了 late / miss / max spike自己的处理时间也很长吗?缩短热路径 / 减少分配 / 去除 I/O存在 DPC / ISR 尖峰吗?检查 USB / Wi-Fi / Bluetooth / GPU / Audio / Storage / ACPI / 驱动更新存在 page fault / GC / 首次开销吗?预先分配 / 预热 / 减少堆负载存在电池 / 省电 / 发热的影响吗?AC 供电 / 电源设置 / 散热 / 长时间测试用 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 排查由驱动引起的波动
  • 温度 / 主频监控 查看发热带来的影响
应用内测量p50 / p95 / p99 / p99.9 / maxmiss / drop / 队列深度ETW / WPR / WPAcontext switch / DPC / ISR / page fault温度 / 主频监控确定改善的优先顺序

用到 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. 参考资料

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

在 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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表