实现实时 1 毫秒准确事件,而不受线程调度的影响
Achieving realtime 1 millisecond accurate events without suffering from thread scheduling
问题
我正在使用 .Net 4.5 创建一个基于 Windows 7 的 C# WPF 应用程序,其主要功能之一是使用一组用户定义的循环时间调用与自定义硬件接口的某些函数。例如,用户可能会选择两个函数每 10 或 20 毫秒调用一次,另一个函数每 500 毫秒调用一次。 用户可选择的最小循环时间为1毫秒。
一开始好像计时很准确,按要求每1毫秒调用一次函数。但我们后来注意到,大约 1-2% 的计时不准确,一些函数被调用仅延迟 5 毫秒,而其他函数可能延迟多达 100 毫秒。即使循环时间大于 1 毫秒,我们也面临着线程在它应该调用外部函数的时候休眠的问题(一个 20 毫秒的函数可能被调用晚了 50 毫秒,因为线程正在休眠并且没有调用该函数)
分析后我们得出结论,这些延迟是零星的,没有明显的模式,这些延迟背后的主要原因可能是 OS 调度和线程上下文切换,换句话说,我们的线程没有唤醒一直如我们所愿。
由于 windows 7 不是 RTOS,我们需要找到是否可以以某种方式解决此问题。但我们确实知道这个问题在 windows 上是可以解决的,因为我们使用具有类似功能的其他工具可以满足这些时序限制,最大容错为 0.7 毫秒。
我们的应用程序是多线程的,最多约有 30 个线程同时 运行ning,其当前峰值 CPU 使用率约为 13%
尝试的解决方案
我们尝试了很多不同的东西,时间主要是使用 秒表计时器 测量的,并且 IsHighResolution 是真的(使用了其他计时器,但我们没有注意到太大的区别):
创建一个单独的线程并赋予其高优先级
结果:无效(同时使用可怕的Thread.Sleep()
,不使用它并使用连续轮询)
使用 C# 任务(线程池)
结果:几乎没有改善
使用周期为 1ms 的多媒体定时器
结果:无效或更糟,多媒体计时器在唤醒 OS 时是准确的,但 OS 可能会选择 运行 另一个线程,没有 1ms保证,但即便如此,偶尔会有更大的延迟
创建了一个单独的独立 C# 项目,其中仅包含一个 while 循环和秒表计时器
结果:大多数时候准确度很高,甚至以微秒为单位,但偶尔线程会休眠
重复第4点,但设置进程优先级为Realtime/High
结果:非常好的数字,几乎没有一条消息有明显的延迟。
结论:
从前面我们发现我们有 5 种可能的行动方案,但我们需要对此类问题有经验的知识渊博的人为我们指明正确的方向:
我们的工具可以优化,线程可以以某种方式管理以确保 1ms 的实时要求。也许部分优化是将工具的进程优先级设置为高或实时,但这似乎不是一个明智的决定,因为用户可能同时使用其他几个工具。
我们将我们的工具分为两个进程,一个包含 GUI 和所有非时间关键操作,另一个包含最少量的时间关键操作并将其设置为 high/real 时间优先,并使用IPC(像WCF)来进行进程间的通信。这可以让我们从两方面受益
其他进程饥饿的可能性较小,因为发生的操作少得多。
该进程将有更少的线程,因此(更少或没有)线程休眠的可能性
注意:接下来的两点将处理内核 space,请注意我对内核 space 和编写驱动程序的了解很少,所以我可能会做出一些错误的假设可以用。
在内核中创建一个驱动程序 space 每 1 毫秒使用较低级别的中断来触发一个事件,强制线程在进程中执行其指定的任务。
将时间关键组件移至内核 space,与程序主体的任何接口都可以通过 API 和回调完成。
也许所有这些都无效,我们可能需要使用像 IntervalZero RTOS 平台这样的 windows RTOS 扩展?
问题本身
我正在寻找两个答案,希望它们有良好的资源支持。
这真的是线程和上下文切换问题吗?还是我们一直漏掉了什么?
5 个选项中的哪一个可以保证解决此问题,如果有多个选项,哪个最简单?如果 none 这些选项可以修复它,什么可以?请记住,我们进行基准测试的其他工具确实在 windows 上达到了所需的计时精度,并且当 CPU 处于重负载下时,100,000 个中的一两个计时可能会偏离不到2毫秒,可以接受。
5 个选项中的哪一个可以保证解决此问题?
这取决于您要达到的准确度。如果您的目标是 +/- 1 毫秒,那么您有合理的机会在没有第 3) 到 5) 点的情况下完成它。要点 1) 和 2) 的组合是要走的路:
- 将您的代码拆分为时间关键部分和时间关键部分(GUI 等),并将它们放入单独的进程中。让他们通过合适的 IPC(管道、共享内存等)进行通信。
- 提高进程优先级class和时间关键进程的线程优先级。不幸的是,c# ThreadPriority Enumeration only permits
THREAD_PRIORITY_HIGHEST(2)
as the maximimum priority. Therefore you'd have to look into the SetThreadPriority function which allows access to THREAD_PRIORITY_TIME_CRITICAL (15)
. The Process::PriorityClass Property 允许访问 REALTIME_PRIORITY_CLASS (24)
。注意:具有此类优先级的代码 运行 会将所有其他代码推开。您必须使代码的计算量非常小并且非常安全。
- 使用 ProcessThread::ProcessorAffinity 属性 调整正确的核心使用。提示:您可能希望让您的时间关键线程远离 CPU_0(属性 值 0x0001),因为 Windows 内核更喜欢此 CPU 进行特定操作。示例:在具有 4 个逻辑处理器的平台上,您将 ProcessoreAffinity 属性 指定为 0x000E 以排除 CPU_0.
- 系统计时器分辨率通常由其他应用程序设置。因此,只有当您指定系统计时器分辨率时,它才是可预测的。一些 applications/drivers 甚至将定时器分辨率设置为 0.5ms。
这可能超出您的设置范围,并可能导致您的应用程序出现问题。
有关如何将计时器分辨率设置为 0.5ms 的信息,请参阅 this SO 答案。 (注意:此分辨率的支持取决于平台。)
一般说明:一切都取决于负载。尽管 Windows 不是 "realtime OS",但它可以做得很好。然而,实时系统也依赖于低负载。什么都不能保证,即使在 RT-OS 负载很重时也是如此。
我怀疑你在 user-mode 中对线程的优先级或亲和力做的任何事情都不能保证你寻求的行为,所以我认为你可能需要类似选项 3 或 4 的东西,这意味着写一个 kernel-mode driver.
在 kernel-mode 中,有 IRQL 的概念,其中在较高级别触发到 运行 的代码抢占较低级别的代码 运行ning。 User-mode 代码 运行 位于 IRQL 0,因此任何更高级别的所有 kernel-mode 代码都优先。线程调度程序本身 运行 处于更高级别,2 我相信(称为 DISPATCH_LEVEL),因此它可以抢占任何优先级的任何调度 user-mode 代码,我相信,包括, REALTIME_PRIORITY_CLASS。包括定时器在内的硬件中断 运行 甚至更高。
如果在较低的 IRQL 下有 CPU/core 可用(higher-level 中断处理程序未执行),硬件计时器将调用其中断处理程序与计时器分辨率一样准确。
如果有很多工作要做,不应该在中断处理程序中完成 (IRQL > DISPATCH_LEVEL),而是使用中断处理程序将更大的工作安排到 运行 "soon" 在 DISPATCH_LEVEL 使用延迟过程调用 (DPC),这仍然可以防止线程调度程序干扰,但不会阻止其他中断处理程序处理它们的硬件中断。
您的选项 3 的一个可能问题是触发事件以唤醒线程到 运行 user-mode 代码在 IRQL 0 是它再次允许线程调度程序决定何时 user-mode 代码将执行。您可能需要在 kernel-mode DISPATCH_LEVEL 完成 time-sensitive 的工作。
另一个问题是中断触发而不考虑 CPU 核心是 运行ning 的进程上下文。因此,当计时器触发时,处理程序可能 运行 在与您的进程无关的进程上下文中。因此,您可能需要使用 kernel-space 内存在 kernel-mode driver 中完成 time-sensitive 工作,独立于您的进程,然后将任何结果反馈回您的应用程序,当它恢复 运行ning 并且可以与 driver 交互时。 (应用程序可以通过 DeviceIoControl API 向下传递缓冲区来与 drivers 交互。)
我不是建议你实现硬件定时器中断处理程序; OS 已经这样做了。相反,使用内核计时器服务根据计时器中断的 OS 处理来调用您的代码。参见 KeSetTimer and ExSetTimer。这两个都可以在计时器触发后在 DISPATCH_LEVEL 回调您的代码。
并且(即使在 kernel-mode 中)默认情况下,系统计时器分辨率对于您的 1 毫秒要求来说可能太粗略了。
https://msdn.microsoft.com/en-us/library/windows/hardware/dn265247(v=vs.85).aspx
For example, for Windows running on an x86 processor, the default interval between system clock ticks is typically about 15 milliseconds
如需更高分辨率,您可以
- 更改系统时钟分辨率
Starting with Windows 2000, a driver can call the ExSetTimerResolution routine to change the time interval between successive system clock interrupts. For example, a driver can call this routine to change the system clock from its default rate to its maximum rate to improve timer accuracy. However, using ExSetTimerResolution has several disadvantages compared to using high-resolution timers created by ExAllocateTimer.
...
- 使用更新的 kernel-mode APIs 用于自动管理时钟分辨率的 high-resolution 计时器。
Starting with Windows 8.1, drivers can use the ExXxxTimer routines to manage high-resolution timers. The accuracy of a high-resolution timer is limited only by the maximum supported resolution of the system clock. In contrast, timers that are limited to the default system clock resolution are significantly less accurate.
However, high-resolution timers require system clock interrupts to—at least, temporarily—occur at a higher rate, which tends to increase power consumption. Thus, drivers should use high-resolution timers only when timer accuracy is essential, and use default-resolution timers in all other cases.
问题
我正在使用 .Net 4.5 创建一个基于 Windows 7 的 C# WPF 应用程序,其主要功能之一是使用一组用户定义的循环时间调用与自定义硬件接口的某些函数。例如,用户可能会选择两个函数每 10 或 20 毫秒调用一次,另一个函数每 500 毫秒调用一次。 用户可选择的最小循环时间为1毫秒。
一开始好像计时很准确,按要求每1毫秒调用一次函数。但我们后来注意到,大约 1-2% 的计时不准确,一些函数被调用仅延迟 5 毫秒,而其他函数可能延迟多达 100 毫秒。即使循环时间大于 1 毫秒,我们也面临着线程在它应该调用外部函数的时候休眠的问题(一个 20 毫秒的函数可能被调用晚了 50 毫秒,因为线程正在休眠并且没有调用该函数)
分析后我们得出结论,这些延迟是零星的,没有明显的模式,这些延迟背后的主要原因可能是 OS 调度和线程上下文切换,换句话说,我们的线程没有唤醒一直如我们所愿。
由于 windows 7 不是 RTOS,我们需要找到是否可以以某种方式解决此问题。但我们确实知道这个问题在 windows 上是可以解决的,因为我们使用具有类似功能的其他工具可以满足这些时序限制,最大容错为 0.7 毫秒。
我们的应用程序是多线程的,最多约有 30 个线程同时 运行ning,其当前峰值 CPU 使用率约为 13%
尝试的解决方案
我们尝试了很多不同的东西,时间主要是使用 秒表计时器 测量的,并且 IsHighResolution 是真的(使用了其他计时器,但我们没有注意到太大的区别):
创建一个单独的线程并赋予其高优先级
结果:无效(同时使用可怕的Thread.Sleep()
,不使用它并使用连续轮询)使用 C# 任务(线程池)
结果:几乎没有改善使用周期为 1ms 的多媒体定时器
结果:无效或更糟,多媒体计时器在唤醒 OS 时是准确的,但 OS 可能会选择 运行 另一个线程,没有 1ms保证,但即便如此,偶尔会有更大的延迟创建了一个单独的独立 C# 项目,其中仅包含一个 while 循环和秒表计时器
结果:大多数时候准确度很高,甚至以微秒为单位,但偶尔线程会休眠重复第4点,但设置进程优先级为Realtime/High
结果:非常好的数字,几乎没有一条消息有明显的延迟。
结论:
从前面我们发现我们有 5 种可能的行动方案,但我们需要对此类问题有经验的知识渊博的人为我们指明正确的方向:
我们的工具可以优化,线程可以以某种方式管理以确保 1ms 的实时要求。也许部分优化是将工具的进程优先级设置为高或实时,但这似乎不是一个明智的决定,因为用户可能同时使用其他几个工具。
我们将我们的工具分为两个进程,一个包含 GUI 和所有非时间关键操作,另一个包含最少量的时间关键操作并将其设置为 high/real 时间优先,并使用IPC(像WCF)来进行进程间的通信。这可以让我们从两方面受益
其他进程饥饿的可能性较小,因为发生的操作少得多。
该进程将有更少的线程,因此(更少或没有)线程休眠的可能性
注意:接下来的两点将处理内核 space,请注意我对内核 space 和编写驱动程序的了解很少,所以我可能会做出一些错误的假设可以用。
在内核中创建一个驱动程序 space 每 1 毫秒使用较低级别的中断来触发一个事件,强制线程在进程中执行其指定的任务。
将时间关键组件移至内核 space,与程序主体的任何接口都可以通过 API 和回调完成。
也许所有这些都无效,我们可能需要使用像 IntervalZero RTOS 平台这样的 windows RTOS 扩展?
问题本身
我正在寻找两个答案,希望它们有良好的资源支持。
这真的是线程和上下文切换问题吗?还是我们一直漏掉了什么?
5 个选项中的哪一个可以保证解决此问题,如果有多个选项,哪个最简单?如果 none 这些选项可以修复它,什么可以?请记住,我们进行基准测试的其他工具确实在 windows 上达到了所需的计时精度,并且当 CPU 处于重负载下时,100,000 个中的一两个计时可能会偏离不到2毫秒,可以接受。
5 个选项中的哪一个可以保证解决此问题?
这取决于您要达到的准确度。如果您的目标是 +/- 1 毫秒,那么您有合理的机会在没有第 3) 到 5) 点的情况下完成它。要点 1) 和 2) 的组合是要走的路:
- 将您的代码拆分为时间关键部分和时间关键部分(GUI 等),并将它们放入单独的进程中。让他们通过合适的 IPC(管道、共享内存等)进行通信。
- 提高进程优先级class和时间关键进程的线程优先级。不幸的是,c# ThreadPriority Enumeration only permits
THREAD_PRIORITY_HIGHEST(2)
as the maximimum priority. Therefore you'd have to look into the SetThreadPriority function which allows access toTHREAD_PRIORITY_TIME_CRITICAL (15)
. The Process::PriorityClass Property 允许访问REALTIME_PRIORITY_CLASS (24)
。注意:具有此类优先级的代码 运行 会将所有其他代码推开。您必须使代码的计算量非常小并且非常安全。 - 使用 ProcessThread::ProcessorAffinity 属性 调整正确的核心使用。提示:您可能希望让您的时间关键线程远离 CPU_0(属性 值 0x0001),因为 Windows 内核更喜欢此 CPU 进行特定操作。示例:在具有 4 个逻辑处理器的平台上,您将 ProcessoreAffinity 属性 指定为 0x000E 以排除 CPU_0.
- 系统计时器分辨率通常由其他应用程序设置。因此,只有当您指定系统计时器分辨率时,它才是可预测的。一些 applications/drivers 甚至将定时器分辨率设置为 0.5ms。 这可能超出您的设置范围,并可能导致您的应用程序出现问题。 有关如何将计时器分辨率设置为 0.5ms 的信息,请参阅 this SO 答案。 (注意:此分辨率的支持取决于平台。)
一般说明:一切都取决于负载。尽管 Windows 不是 "realtime OS",但它可以做得很好。然而,实时系统也依赖于低负载。什么都不能保证,即使在 RT-OS 负载很重时也是如此。
我怀疑你在 user-mode 中对线程的优先级或亲和力做的任何事情都不能保证你寻求的行为,所以我认为你可能需要类似选项 3 或 4 的东西,这意味着写一个 kernel-mode driver.
在 kernel-mode 中,有 IRQL 的概念,其中在较高级别触发到 运行 的代码抢占较低级别的代码 运行ning。 User-mode 代码 运行 位于 IRQL 0,因此任何更高级别的所有 kernel-mode 代码都优先。线程调度程序本身 运行 处于更高级别,2 我相信(称为 DISPATCH_LEVEL),因此它可以抢占任何优先级的任何调度 user-mode 代码,我相信,包括, REALTIME_PRIORITY_CLASS。包括定时器在内的硬件中断 运行 甚至更高。
如果在较低的 IRQL 下有 CPU/core 可用(higher-level 中断处理程序未执行),硬件计时器将调用其中断处理程序与计时器分辨率一样准确。
如果有很多工作要做,不应该在中断处理程序中完成 (IRQL > DISPATCH_LEVEL),而是使用中断处理程序将更大的工作安排到 运行 "soon" 在 DISPATCH_LEVEL 使用延迟过程调用 (DPC),这仍然可以防止线程调度程序干扰,但不会阻止其他中断处理程序处理它们的硬件中断。
您的选项 3 的一个可能问题是触发事件以唤醒线程到 运行 user-mode 代码在 IRQL 0 是它再次允许线程调度程序决定何时 user-mode 代码将执行。您可能需要在 kernel-mode DISPATCH_LEVEL 完成 time-sensitive 的工作。
另一个问题是中断触发而不考虑 CPU 核心是 运行ning 的进程上下文。因此,当计时器触发时,处理程序可能 运行 在与您的进程无关的进程上下文中。因此,您可能需要使用 kernel-space 内存在 kernel-mode driver 中完成 time-sensitive 工作,独立于您的进程,然后将任何结果反馈回您的应用程序,当它恢复 运行ning 并且可以与 driver 交互时。 (应用程序可以通过 DeviceIoControl API 向下传递缓冲区来与 drivers 交互。)
我不是建议你实现硬件定时器中断处理程序; OS 已经这样做了。相反,使用内核计时器服务根据计时器中断的 OS 处理来调用您的代码。参见 KeSetTimer and ExSetTimer。这两个都可以在计时器触发后在 DISPATCH_LEVEL 回调您的代码。
并且(即使在 kernel-mode 中)默认情况下,系统计时器分辨率对于您的 1 毫秒要求来说可能太粗略了。
https://msdn.microsoft.com/en-us/library/windows/hardware/dn265247(v=vs.85).aspx
For example, for Windows running on an x86 processor, the default interval between system clock ticks is typically about 15 milliseconds
如需更高分辨率,您可以
- 更改系统时钟分辨率
Starting with Windows 2000, a driver can call the ExSetTimerResolution routine to change the time interval between successive system clock interrupts. For example, a driver can call this routine to change the system clock from its default rate to its maximum rate to improve timer accuracy. However, using ExSetTimerResolution has several disadvantages compared to using high-resolution timers created by ExAllocateTimer.
...
- 使用更新的 kernel-mode APIs 用于自动管理时钟分辨率的 high-resolution 计时器。
Starting with Windows 8.1, drivers can use the ExXxxTimer routines to manage high-resolution timers. The accuracy of a high-resolution timer is limited only by the maximum supported resolution of the system clock. In contrast, timers that are limited to the default system clock resolution are significantly less accurate.
However, high-resolution timers require system clock interrupts to—at least, temporarily—occur at a higher rate, which tends to increase power consumption. Thus, drivers should use high-resolution timers only when timer accuracy is essential, and use default-resolution timers in all other cases.