C# .NET/Mono中精确可靠的步进时间控制

5
为了我正在处理的一个项目,我需要每秒执行一些逻辑(几乎)精确地10次。我知道非实时操作系统的限制,偶尔的10-20%的余量是可以接受的;也就是说,在周期之间偶尔出现长达120毫秒的延迟是可以接受的。然而,重要的是我能够绝对保证逻辑执行的周期性,并且不会发生超出上述范围的延迟。这似乎在C#中很难实现。
我的情况如下:应用程序启动后的一段时间内,触发一个事件,该事件将开始逻辑执行周期。当该周期运行时,程序还处理其他任务,例如通信、日志记录等。 我需要能够在Windows上使用.NET和在Linux上使用Mono运行程序。这排除了导入winmm.dll作为使用其高精度计时函数的可能性。
我到目前为止尝试过的方法:
- 使用while循环,使用Stopwatch计算逻辑执行后所需的剩余延迟时间,然后调用Thread.Sleep以该延迟时间为参数;这种方法非常不可靠,通常会导致更长的延迟,并偶尔导致非常长的延迟。 - 使用System.Threading.Timer;回调通常每109ms调用一次。 - 使用System.Timers.Timer,我认为这更合适,并将AutoReset设置为true;Elapsed事件每109ms被触发一次。 - 使用高精度计时器,例如可以在这里这里找到的计时器。然而,这会导致非常高的CPU负载,这不符合我的系统设计要求。
到目前为止,最好的选择似乎是使用System.Timers.Timer类。为了纠正上述109ms,我将间隔设置为92ms(这似乎非常hacky...!)。然后,在事件处理程序中,我使用Stopwatch计算实际经过的时间,然后根据该计算执行我的系统逻辑。
代码如下:
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
@BoeseB:感谢您的回复。如果使用Winmm.ddl函数也能保证不会出现长时间间隔,我可能最终会回到Windows上使用它。这就留下了一个问题,如何在Linux下使用Mono来处理这个问题。 - mennowo
你在逻辑执行中做什么?你是在尝试控制像PLC那样的机器吗?还是这个循环逻辑的目的是什么? - BoeseB
这涉及到一个交通信号灯控制器;根据当前系统状态,它计算输出的期望状态。它实际上并不控制任何物理设备,只是进行计算并设置状态。然后程序通过远程通信传递新状态。 - mennowo
1个回答

2

同时,我已经基本解决了这个问题。

首先,我在之前提到的计时器的CPU使用率上做出了更正。CPU使用率是由于我的自己的代码,其中我使用了一个紧密的while循环。

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

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

这通常是在Linux环境下成立的。

现在,可以使用两个不同的计时器,例如这个用于Windows,在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毫秒范围内,间隔设置为100毫秒。对于我的目的来说甚至更加重要的是,没有更长的时间间隔。
在较慢的系统(树莓派第一代B型号)上,我仍然偶尔会遇到更长的时间间隔,但在得出结论之前,我必须先检查总体效率。
还有这个计时器,它可以在两个操作系统下立即使用。在一个测试程序中,与先前链接的计时器相比,在Linux和Mono下导致了更高的CPU负载。

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