实现1毫秒精度的实时事件,而不受线程调度影响。

9

问题

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

起初似乎计时准确,函数按照所需的每1毫秒调用。但后来我们注意到,大约1-2%的计时不准确,有些函数延迟了5毫秒,而其他函数则可能延迟高达100毫秒。即使周期时间大于1毫秒,我们也面临着这样的问题:线程在应该调用外部函数的时间上睡眠(一个20毫秒的函数可能会因为线程睡眠而被延迟50毫秒才能调用函数)。

经过分析,我们得出结论,这些延迟是零散的,没有明显的模式,并且这些延迟的主要可能原因是操作系统调度和线程上下文切换,换句话说,我们的线程不能始终保持唤醒状态。

由于Windows 7不是实时操作系统,因此我们需要找到是否可以以某种方式解决此问题。但我们确信这个问题在Windows上是可以解决的,因为我们使用其他具有类似功能的工具,可以满足这些时间限制,并且最大容差为0.7毫秒。

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

尝试的解决方案

我们尝试了许多不同的方法,主要使用秒表计时器进行计时,IsHighResolution为true(使用了其他计时器,但我们没有注意到太大的区别):

  1. 创建一个独立的线程并将其设置为高优先级
    结果: 无效(使用可怕的Thread.Sleep(),以及不使用它并使用持续轮询)

  2. 使用C#任务(线程池)
    结果: 改善很小

  3. 使用1ms周期性的多媒体定时器
    结果: 无效或更糟,多媒体定时器在唤醒操作系统方面很准确,但操作系统可能选择运行另一个线程,没有1ms的保证,即使是这样,偶尔延迟也可能更大

  4. 创建一个独立的C#项目,其中只包含while循环和秒表计时器
    结果: 大多数时间精确度非常好,甚至达到微秒级别,但有时线程会休眠

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

结论:

从前面我们发现有5种可能的解决方案,但我们需要有经验的专业人员来指点方向:

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

  2. 我们将工具分成两个进程,一个包含GUI和所有非时间关键操作,另一个包含最少量的时间关键操作,并将其设置为高/实时优先级,并使用IPC(如WCF)在进程之间进行通信。这样做有两个好处:

    1. 其他进程饿死的可能性更小,因为发生的操作更少。

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

注意:下面两个点将涉及内核空间,请注意我对内核空间和编写驱动程序了解甚少,因此我可能对如何使用它们做出错误的假设。

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

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

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

问题本身

我正在寻找两个答案,并希望它们有可靠的来源支持。
1. 这是否真的是线程和上下文切换问题?或者我们一直缺少了什么?
2. 五个选项中哪一个保证可以解决这个问题,如果有几个选项可以,哪一个最容易实现?如果没有这些选项可以解决它,还有什么其它方法?请记住,我们已经与其他工具进行了基准测试,这些工具在Windows系统上确实达到所需的时间精度,并且当CPU负载较重时,100,000个定时中可能会有1或2个定时偏差小于2毫秒,这是非常可以接受的。

你认为哪段代码有问题?在一个单独的线程中使用紧密循环(while true)应该尽量减少切换次数。 - Patrick Hofman
3
你需要一个实时操作系统(Real-time OS)... - Matthew Watson
@PatrickHofman 但实际情况并非如此,有时确实会被切换,这不是正常行为吗? - Ahmed Agamy
@MatthewWatson 但是其他人已经做到了。 - Ahmed Agamy
你可以研究一下MCCSS,这提供了一些适用于多媒体等时间敏感代码的保证。但正如其他人所说,如果没有实时操作系统,就无法获得100%的时间片保障。 - Ryan Bemrose
2个回答

8

哪个选项可以保证解决这个问题?

这取决于您试图实现的精度。如果您的目标是+/- 1毫秒,那么在不使用第3到5个点的情况下,您有很大的机会完成它。点1和点2的组合是正确的方式:

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

一般备注:一切都取决于负载。尽管Windows不是“实时操作系统”,但它仍然可以做得很好。但是,即使在RT-OS上严重负载时也没有保证。


那是非常详细和有用的回答,Arno,非常感谢。我会尝试您的建议并返回反馈。 - Ahmed Agamy
经过尝试了几种方法后,结果证明您的建议足以实现1毫秒精度的软实时。我比以前少了很多问题,但这可能是因为我仍然没有分离两个进程,所以当我执行并优化我的代码时,它应该会有所改善。谢谢。 - Ahmed Agamy
@AhmedAgamy,我们正在研究与您所描述的要求非常相似的问题。您能否分享一些关于如何执行“实时”进程的信息/代码细节?例如,您是否需要设置PriorityClass、Affinity等?计时器方面如何处理——您是否使用系统计时器?您是从“gui”进程启动进程吗?任何信息都可以帮助我们避免从头开始... - Werner

4
我怀疑你在用户模式下对线程的优先级或亲和力所做的任何操作都不能保证你所寻求的行为,所以我认为你可能需要像选项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向下传递缓冲区来与驱动程序交互。)
我并不建议您实现硬件定时器中断处理程序;操作系统已经完成了这一点。相反,使用内核计时器服务根据操作系统处理定时器中断的情况来调用您的代码。请参阅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有几个缺点。
2. 使用新的内核模式API进行高分辨率定时器,这些API自动管理时钟分辨率。
从Windows 8.1开始,驱动程序可以使用ExXxxTimer例程来管理高分辨率定时器。高分辨率定时器的精度仅受系统时钟支持的最大分辨率限制。相比之下,仅限于默认系统时钟分辨率的定时器要不精确得多。
但是,高分辨率定时器需要系统时钟中断以至少暂时发生在更高的频率,这往往会增加功耗。因此,驱动程序应只在计时器准确性至关重要时使用高分辨率定时器,并在所有其他情况下使用默认分辨率计时器。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接