8

问题

我正在使用.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是正确的(使用了其他计时器,但我们没有发现太大差异):

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

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

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

  4. 创建了一个单独的独立 C# 项目,其中仅包含一个 while 循环和秒表计时器
    结果:大多数情况下,即使在微秒内,精度也很好,但偶尔线程会休眠

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

结论:

从前面我们发现我们有 5 种可能的行动方案,但我们需要有此类问题经验的知识渊博的人来为我们指明正确的方向:

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

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

    1. 由于发生的操作少得多,其他进程出现饥饿的可能性更小。

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

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

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

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

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

问题本身

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

  1. 这真的是一个线程和上下文切换问题吗?还是我们一直在遗漏一些东西?

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

4

2 回答 2

7

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

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

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

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

于 2016-07-28T13:15:24.083 回答
4

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

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

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

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

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

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

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

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

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

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

对于更高的分辨率,您可以

  1. 更改系统时钟分辨率

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

...

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

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

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

于 2016-07-29T17:45:11.933 回答