在不受线程调度影响的情况下实现 1 毫秒的实时准确事件

Posted

技术标签:

【中文标题】在不受线程调度影响的情况下实现 1 毫秒的实时准确事件【英文标题】:Achieving realtime 1 millisecond accurate events without suffering from thread scheduling 【发布时间】:2016-12-02 18:11:36 【问题描述】:

问题

我正在使用 .Net 4.5 创建一个基于 Windows 7 的 C# WPF 应用程序,其主要功能之一是调用与自定义硬件接口的某些函数 具有一组用户定义的循环时间。例如,用户可以选择每 10 或 20 毫秒调用两个函数,每 500 毫秒调用另一个函数。 用户可以选择的最小循环时间为 1 毫秒。

起初,时间似乎是准确的,并且根据需要每 1 毫秒调用一次函数。但我们后来注意到,大约有 1-2% 的计时不准确,有些函数被调用时仅延迟了 5 毫秒,而其他函数可能延迟了 100 毫秒。即使循环时间大于 1 毫秒,我们也面临线程在它应该调用外部函数的时间休眠的问题(20 毫秒的函数可能会延迟 50 毫秒,因为线程正在休眠并且没有调用该函数)

经过分析,我们得出结论,这些延迟是零星的,没有明显的模式,并且这些延迟背后的主要原因可能是操作系统调度和线程上下文切换,换句话说,我们的线程并没有像我们需要的那样一直处于唤醒状态它是。

由于 Windows 7 不是 RTOS,我们需要确定是否可以通过某种方式解决此问题。但我们确实知道这个问题在 Windows 上是可以解决的,因为我们使用具有类似功能的其他工具可以满足最大 0.7 毫秒容错的时间限制。

我们的应用程序是多线程的,最多同时运行大约 30 个线程,其当前的峰值 CPU 使用率约为 13%

尝试的解决方案

我们尝试了很多不同的方法,主要是使用 秒表计时器 来测量计时,并且 IsHighResolution 是正确的(使用了其他计时器,但我们没有发现太大差异):

    创建一个单独的线程并为其赋予高优先级 结果:无效(使用可怕的Thread.Sleep(),没有它并使用连续轮询)

    使用 C# 任务(线程池) 结果:几乎没有改善

    使用具有 1 毫秒周期的多媒体计时器 结果:无效或更糟,多媒体定时器在唤醒操作系统时是准确的,但操作系统可能会选择运行另一个线程,没有 1ms 保证,但即便如此,有时延迟可能会更大

    创建了一个单独的独立 C# 项目,其中仅包含一个 while 循环和秒表计时器 结果:在大多数情况下,准确度非常好,甚至以微秒为单位,但偶尔线程会休眠

    重复第 4 点,但将进程优先级设置为实时/高 结果:非常好的数字,几乎没有一条消息有明显的延迟。

结论:

从前面我们发现我们有 5 种可能的行动方案,但我们需要在这些问题上具有丰富经验的人来为我们指明正确的方向:

    我们的工具可以进行优化,并以某种方式管理线程以确保 1 毫秒的实时要求。也许优化的一部分是将工具的进程优先级设置为高或实时,但这似乎不是一个明智的决定,因为用户可能同时使用多个其他工具。

    我们将我们的工具分为两个进程,一个包含 GUI 和所有非时间关键操作,另一个包含最少量的时间关键操作并将其设置为高/实时优先级,并使用 IPC(如WCF) 用于进程之间的通信。这可以从两个方面使我们受益

      由于发生的操作更少,其他进程的饥饿概率也更低。

      进程将拥有更少的线程,因此(更少或没有)线程休眠的可能性

注意:接下来的两点将涉及内核空间,请注意我对内核空间和编写驱动程序的信息很少,因此我可能对如何使用它做出了一些错误的假设。

    在内核空间中创建一个驱动程序,该驱动程序每 1 毫秒使用一次较低级别的中断来触发一个事件,强制线程在进程中执行其指定的任务。

    将时间关键组件移至内核空间,与程序主体的任何接口都可以通过 API 和回调完成。

    也许所有这些都无效,我们可能需要使用 Windows RTOS 扩展,如 IntervalZero RTOS 平台?

问题本身

我正在寻找两个答案,我希望它们得到良好资源的支持。

    这真的是线程和上下文切换问题吗?还是我们一直以来都遗漏了什么?

    5 个选项中的哪一个可以保证解决这个问题,如果有几个,哪个是最简单的?如果这些选项都不能解决它,那有什么可以解决的呢?请记住,我们基准测试过的其他工具在 windows 上确实达到了所需的计时精度,并且当 CPU 处于重负载下时,100,000 次中的一到两次可能会关闭不到 2 毫秒,这是非常可以接受的。

【问题讨论】:

你认为哪个代码有问题?单独线程中的紧密循环 (while true) 应始终以尽可能少的切换运行。 你需要一个实时操作系统... @PatrickHofman 但这不是实际发生的情况,它有时会被关闭,这不是正常行为吗? @MatthewWatson 但其他人已经做到了。 您可以查看MMCSS,它为多媒体等时间敏感代码提供了一些保证。但正如其他人所说,没有实时操作系统就无法获得 100% 保证的时间片。 【参考方案1】:

5 个选项中的哪一个可以保证解决这个问题?

这取决于您要达到的准确度。如果您的目标是 +/- 1 毫秒,那么您有合理的机会在没有第 3) 到第 5) 点的情况下完成它。 1)和2)点的结合是要走的路:

将代码拆分为时间关键部分和时间关键部分(GUI 等),并将它们放入单独的进程中。让它们通过体面的 IPC(管道、共享内存等)进行通信。 提高时间关键进程的进程优先级和线程优先级。不幸的是,c# ThreadPriority Enumeration 只允许 THREAD_PRIORITY_HIGHEST(2) 作为最大优先级。因此,您必须查看允许访问THREAD_PRIORITY_TIME_CRITICAL (15) 的SetThreadPriority 函数。 Process::PriorityClass Property 允许访问REALTIME_PRIORITY_CLASS (24)。注意:以此类优先级运行的代码会将所有其他代码排除在外。您必须使代码的计算量非常小并且非常安全。 使用ProcessThread::ProcessorAffinity 属性调整正确的内核使用率。提示:您可能希望使时间关键线程远离 CPU_0(属性值 0x0001),因为 Windows 内核更喜欢此 CPU 用于特定操作。示例:在具有 4 个逻辑处理器的平台上,您需要将 ProcessoreAffinity 属性指定为 0x000E 以排除 CPU_0。 系统计时器分辨率通常由其他应用程序设置。因此,只有当您指定系统计时器分辨率时,它才是可预测的。一些应用程序/驱动程序甚至将定时器分辨率设置为 0.5 毫秒。 这可能超出您的设置,并可能导致您的应用程序出现问题。 有关如何将计时器分辨率设置为 0.5 毫秒的信息,请参阅this SO 答案。 (注意:此分辨率的支持取决于平台。)

一般说明:一切都取决于负载。尽管它不是“实时操作系统”,但 Windows 可以做得很好。然而,实时系统也依赖于低负载。什么都不能保证,即使是在负载很重的 RT-OS 上也是如此。

【讨论】:

这是一个非常翔实和详细的答案阿诺,非常感谢。我会尝试您的建议并返回提供反馈。 在尝试了几件事之后,事实证明您的建议足以实现 1ms 精度的软实时。我的问题比以前少了很多,但这可能是因为我仍然没有将这两个进程分开,所以当我执行和优化我的代码时,它应该几乎可以改进它。谢谢 @AhmedAgamy 我们正在研究与您在此处描述的大致相同的要求。您能否分享一些有关如何执行“实时”过程的信息/代码细节?例如。您是否必须设置 PriorityClass、Affinity 等?那么计时器呢?您是否使用系统计时器进行管理?你是从“gui”过程开始的吗?任何信息都可以帮助我们不必从头开始......【参考方案2】:

我怀疑你在用户模式下对线程的优先级或亲和性所做的任何事情都不会保证你寻求的行为,所以我认为你可能需要类似于选项 3 或 4 的东西,这意味着编写内核模式驱动程序。

在内核模式中,有 IRQL 的概念,其中触发运行在较高级别的代码会抢占在较低级别运行的代码。用户模式代码以 IRQL 0 运行,因此任何更高级别的所有内核模式代码都具有优先权。线程调度程序本身运行在更高的级别,我相信 2(称为 DISPATCH_LEVEL),因此它可以抢占任何优先级的任何调度的用户模式代码,我相信包括 REALTIME_PRIORITY_CLASS。包括定时器在内的硬件中断运行得更高。

如果在较低的 IRQL 上有可用的 CPU/内核(更高级别的中断处理程序未执行),硬件计时器将调用其中断处理程序,其精度与计时器分辨率一样。

如果有很多工作要做,不应该在中断处理程序中执行 (IRQL > DISPATCH_LEVEL),而是使用中断处理程序来安排更大的工作主体,以便使用延迟过程“很快”在 DISPATCH_LEVEL 运行调用 (DPC),它仍然会阻止线程调度程序进行干扰,但不会阻止其他中断处理程序处理其硬件中断。

您的选项 3 的一个可能问题是触发事件以唤醒线程以在 IRQL 0 处运行用户模式代码是它再次允许线程调度程序决定何时执行用户模式代码。您可能需要在 DISPATCH_LEVEL 的内核模式下进行时间敏感的工作。

另一个问题是中断触发与 CPU 内核运行的进程上下文无关。因此,当计时器触发时,处理程序可能会在与您无关的进程的上下文中运行。因此,您可能需要在内核模式驱动程序中进行时间敏感的工作,使用内核空间内存,独立于您的进程,然后稍后将任何结果反馈给您的应用程序,当它恢复运行并可以与驱动程序交互时. (应用程序可以通过 DeviceIoControl API 向下传递缓冲区来与驱动程序交互。)

我并不是建议你实现一个硬件定时器中断处理程序;操作系统已经这样做了。相反,使用内核定时器服务根据操作系统对定时器中断的处理来调用您的代码。请参阅 KeSetTimer 和 ExSetTimer。这两个都可以在定时器触发后在 DISPATCH_LEVEL 回调你的代码。

而且(即使在内核模式下)默认情况下,系统计时器分辨率可能对于您的 1 毫秒要求来说太粗糙了。

https://msdn.microsoft.com/en-us/library/windows/hardware/dn265247(v=vs.85).aspx

例如,对于在 x86 处理器上运行的 Windows,系统时钟节拍之间的默认间隔通常约为 15 毫秒

如果需要更高的分辨率,您可以

    更改系统时钟分辨率

从 Windows 2000 开始,驱动程序可以调用 ExSetTimerResolution 例程来更改连续系统时钟中断之间的时间间隔。例如,驱动程序可以调用此例程将系统时钟从其默认速率更改为其最大速率,以提高计时器的准确性。但是,与使用 ExAllocateTimer 创建的高分辨率计时器相比,使用 ExSetTimerResolution 有几个缺点。

...

    将更新的内核模式 API 用于自动管理时钟分辨率的高分辨率计时器。

从 Windows 8.1 开始,驱动程序可以使用 ExXxxTimer 例程来管理高分辨率计时器。高分辨率定时器的精度仅受系统时钟支持的最大分辨率限制。相比之下,仅限于默认系统时钟分辨率的计时器的准确度要低得多。

但是,高分辨率定时器需要系统时钟中断(至少是暂时的)以更高的速率发生,这往往会增加功耗。因此,驱动程序应仅在计时器精度至关重要时使用高分辨率计时器,并在所有其他情况下使用默认分辨率计时器。

【讨论】:

以上是关于在不受线程调度影响的情况下实现 1 毫秒的实时准确事件的主要内容,如果未能解决你的问题,请参考以下文章

如何在不完全影响主屏幕绘图性能的情况下每 200/250 毫秒更新一次小部件?

毫秒,微妙级别软件定时器

线程控制

如何在不影响实时功能的情况下为 React 和 React-native 托管通用的 Firebase 后端代码?

如何在不影响剩余 TTL 的情况下更新 redis 值?

在不真正更改数据的情况下发出多个提交时的性能影响