C# .NET/Mono 中精确可靠的步进计时

Posted

技术标签:

【中文标题】C# .NET/Mono 中精确可靠的步进计时【英文标题】:Precise and reliable step timing in C# .NET/Mono 【发布时间】:2017-12-19 07:28:32 【问题描述】:

对于我正在进行的项目,我需要每秒(几乎)准确地执行一些逻辑 10 次。我知道非实时操作系统的局限性,偶尔 10-20% 的余量是可以的;也就是说,周期之间偶尔延迟最多 120 毫秒是可以的。但是,重要的是我可以绝对保证周期性的逻辑执行,并且不会出现超出上述余量的延迟。这在 C# 中似乎很难实现。

我的情况如下:应用程序启动后的一段时间,触发了一个事件,将启动逻辑执行周期。在该循环运行时,该程序还处理其他任务,例如通信、日志记录等。 我需要能够在 Windows 上使用 .NET 和在 Linux 上使用 Mono 运行该程序。这不包括导入 winmm.dll 作为使用其高精度计时功能的可能性。

到目前为止我尝试了什么:

使用 while 循环,使用 Stopwatch 计算逻辑执行后所需的剩余延迟,然后以该延迟量调用 Thread.Sleep;这是非常不可靠的,通常会导致更长的延迟,有时会导致很长的延迟 使用 System.Threading.Timer;回调通常每 ~109 毫秒调用一次 使用我认为更合适的 System.Timers.Timer,并将 AutoReset 设置为 true; Elapsed 事件每约 109 毫秒引发一次。 使用高精度计时器,例如可以在here 或here 找到的计时器。但是,这会导致(正如预期的那样)非常高的 cpu 负载,考虑到我的系统设计,这是不可取的。

目前最好的选择似乎是使用 System.Timers.Timer 类。为了更正上面提到的 109 毫秒,我将间隔设置为 92 毫秒(这看起来很 hacky ......!)。然后,在事件处理程序中,我使用秒表计算实际经过的时间,然后根据该计算执行我的系统逻辑。

在代码中:

var timer = new System.Timers.Timer(92);
timer.Elapsed += TimerElapsed;
timer.AutoReset = true;
timer.Start();
while (true)

和处理程序:

private void TimerElapsed(object sender, ElapsedEventArgs e)

    var elapsed = _watch.ElapsedMilliseconds;
    _watch.Restart();
    DoWork(elapsed);

但是,即使使用这种方法,偶尔也会发生事件仅在超过 200 毫秒后才触发,最多超过 500 毫秒(在 Mono 上)。这意味着我错过了一个或多个逻辑执行周期,这可能是有害的。

有没有更好的方法来解决这个问题?或者这个问题是操作系统工作方式所固有的,没有更可靠的方法可以在没有高 CPU 负载的情况下以稳定的间隔执行重复的逻辑执行?

【问题讨论】:

我遇到了类似的问题,最终在 Winmm.dll 中使用了本机计时功能。可用 .NET 计时器的最大精度为 +-14ms @BoeseB:感谢您的回复。我可能最终会恢复使用 Windows 的 Winmm.ddl 函数,如果这也保证不会发生长间隔。这就留下了如何在 Linux 下使用 Mono 来处理这个问题。 你在逻辑执行中做什么?您是否尝试像 PLC 一样控制某些机器,或者这种循环逻辑的目的是什么? 这涉及交通灯控制器;基于当前系统状态,它计算输出的期望状态。它实际上并没有物理控制任何东西,只是进行计算和设置状态。然后程序远程传达新的状态。 【参考方案1】:

与此同时,我能够在很大程度上解决这个问题。

首先,我对问题中引用的计时器的 CPU 使用情况进行了纠正。 CPU 使用率是由于我自己的代码,我使用了一个紧凑的 while 循环。

发现后,我能够通过使用两个计时器来解决该问题,并在运行时检查环境类型,以决定实际使用哪一个。为了检查环境,我使用:

private static readonly bool IsPosixEnvironment = Path.DirectorySeparatorChar == '/';

这在 Linux 下通常是正确的。

现在,可以使用两个不同的计时器,例如this one 用于 Windows,this one 用于 linux,如下所示:

if (IsPosixEnvironment)

    _linTimer = new PosixHiPrecTimer();
    _linTimer.Tick += LinTimerElapsed;
    _linTimer.Interval = _stepsize;
    _linTimer.Enabled = true;

else

    _winTimer = new WinHiPrecTimer();
    _winTimer.Elapsed += WinTimerElapsed;
    _winTimer.Interval = _stepsize;
    _winTimer.Resolution = 25;
    _winTimer.Start();

到目前为止,这给了我很好的结果;步长通常在 99-101 ms 范围内,间隔设置为 100 ms。此外,对于我而言,更重要的是,不再有间隔。

在较慢的系统(Raspberry Pi 第一代 B 型)上,我仍然偶尔会得到更长的间隔,但我必须先检查整体效率,然后再得出结论。

还有this timer,它可以在两种操作系统下开箱即用。在一个测试程序中,与之前链接的那个相比,这个在带有 Mono 的 Linux 下导致了更高的 CPU 负载。

【讨论】:

以上是关于C# .NET/Mono 中精确可靠的步进计时的主要内容,如果未能解决你的问题,请参考以下文章

C# 版本的 计时器类:精确到微秒 秒后保留一位小数 支持年月日时分秒带单位的输出

C++中如何精确计时

STM32的PWM控制步进电机,怎么实现精确控制输出脉冲数

在C语言中如何实现精确计时

.NET 中的精确计时

网络音频 api 计时是不是比 setTimeout 更精确