C# 中精确可靠的步进计时。NET/Mono
Precise and reliable step timing in C# .NET/Mono
对于我正在从事的项目,我需要每秒(几乎)正好执行 10 次某些逻辑。我知道非实时操作系统的局限性,偶尔 10-20% 的余量是可以的;也就是说,周期之间偶尔延迟最多 120 毫秒是可以的。但是,重要的是我可以绝对保证周期性的逻辑执行,并且不会出现超出上述裕度的延迟。这在 C# 中似乎很难实现。
我的情况是这样的:应用程序启动一段时间后,触发了一个事件,将开始逻辑执行周期。在那个周期 运行s 的同时,程序还处理其他任务,例如通信、日志记录等。
我需要能够 运行 使用 .NET Windows 和 Mono Linux 的程序。这不包括导入 winmm.dll 作为使用其高精度计时功能的可能性。
到目前为止我尝试了什么:
- 使用 while 循环,使用秒表计算逻辑执行后所需的剩余延迟,然后用该延迟量调用 Thread.Sleep;这是非常不可靠的,通常会导致更长的延迟,偶尔会导致很长的延迟
- 使用System.Threading.Timer;回调通常每 ~109 毫秒调用一次
- 使用我认为更合适的System.Timers.Timer,并将AutoReset设置为true; Elapsed 事件每 ~109 毫秒引发一次。
- 使用高精度计时器,例如可以找到的计时器 here or here。但是,这会导致(正如预期的那样)非常高的 cpu 负载,考虑到我的系统设计,这是不可取的。
到目前为止最好的选择似乎是使用 System.Timers.Timer class。为了纠正提到的 109 毫秒,我将间隔设置为 92 毫秒(这看起来很老套......!)。然后,在事件处理程序中,我使用秒表计算实际经过的时间,然后根据该计算执行我的系统逻辑。
在代码中:
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 毫秒(在单声道上)。这意味着我错过了一个或多个逻辑执行周期,这可能是有害的。
有没有更好的方法来处理这个问题?还是这个问题是 OS 工作方式固有的,是否没有更可靠的方法来在没有高 CPU 负载的情况下以稳定的间隔进行重复逻辑执行?
与此同时,我基本上解决了这个问题。
首先,我对我在问题中引用的计时器的 CPU 用法进行了更正。 CPU 使用是由于我自己的代码,我在其中使用了一个紧凑的 while 循环。
发现后,我可以通过使用两个计时器来解决问题,并在运行时检查环境类型,以决定实际使用哪一个。为了检查环境,我使用:
private static readonly bool IsPosixEnvironment = Path.DirectorySeparatorChar == '/';
这在 Linux 下通常是正确的。
现在可以使用两个不同的定时器,比如this one for Windows, and 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 毫秒范围内,间隔设置为 100 毫秒。此外,对我来说更重要的是,不再有更长的间隔。
在较慢的系统(Raspberry Pi 第一代模型 B)上,我仍然偶尔会有更长的间隔,但在得出结论之前我必须先检查整体效率。
还有 this timer,它可以在两种操作系统下开箱即用。在一个测试程序中,与之前链接的程序相比,这个程序在 Linux 和 Mono 下导致更高的 CPU 负载。
对于我正在从事的项目,我需要每秒(几乎)正好执行 10 次某些逻辑。我知道非实时操作系统的局限性,偶尔 10-20% 的余量是可以的;也就是说,周期之间偶尔延迟最多 120 毫秒是可以的。但是,重要的是我可以绝对保证周期性的逻辑执行,并且不会出现超出上述裕度的延迟。这在 C# 中似乎很难实现。
我的情况是这样的:应用程序启动一段时间后,触发了一个事件,将开始逻辑执行周期。在那个周期 运行s 的同时,程序还处理其他任务,例如通信、日志记录等。 我需要能够 运行 使用 .NET Windows 和 Mono Linux 的程序。这不包括导入 winmm.dll 作为使用其高精度计时功能的可能性。
到目前为止我尝试了什么:
- 使用 while 循环,使用秒表计算逻辑执行后所需的剩余延迟,然后用该延迟量调用 Thread.Sleep;这是非常不可靠的,通常会导致更长的延迟,偶尔会导致很长的延迟
- 使用System.Threading.Timer;回调通常每 ~109 毫秒调用一次
- 使用我认为更合适的System.Timers.Timer,并将AutoReset设置为true; Elapsed 事件每 ~109 毫秒引发一次。
- 使用高精度计时器,例如可以找到的计时器 here or here。但是,这会导致(正如预期的那样)非常高的 cpu 负载,考虑到我的系统设计,这是不可取的。
到目前为止最好的选择似乎是使用 System.Timers.Timer class。为了纠正提到的 109 毫秒,我将间隔设置为 92 毫秒(这看起来很老套......!)。然后,在事件处理程序中,我使用秒表计算实际经过的时间,然后根据该计算执行我的系统逻辑。
在代码中:
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 毫秒(在单声道上)。这意味着我错过了一个或多个逻辑执行周期,这可能是有害的。
有没有更好的方法来处理这个问题?还是这个问题是 OS 工作方式固有的,是否没有更可靠的方法来在没有高 CPU 负载的情况下以稳定的间隔进行重复逻辑执行?
与此同时,我基本上解决了这个问题。
首先,我对我在问题中引用的计时器的 CPU 用法进行了更正。 CPU 使用是由于我自己的代码,我在其中使用了一个紧凑的 while 循环。
发现后,我可以通过使用两个计时器来解决问题,并在运行时检查环境类型,以决定实际使用哪一个。为了检查环境,我使用:
private static readonly bool IsPosixEnvironment = Path.DirectorySeparatorChar == '/';
这在 Linux 下通常是正确的。
现在可以使用两个不同的定时器,比如this one for Windows, and 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 毫秒范围内,间隔设置为 100 毫秒。此外,对我来说更重要的是,不再有更长的间隔。
在较慢的系统(Raspberry Pi 第一代模型 B)上,我仍然偶尔会有更长的间隔,但在得出结论之前我必须先检查整体效率。
还有 this timer,它可以在两种操作系统下开箱即用。在一个测试程序中,与之前链接的程序相比,这个程序在 Linux 和 Mono 下导致更高的 CPU 负载。