c# - 结合秒表和计时器,通过平滑强制获得长期准确的事件

标签 c# timer stopwatch

首先,我要说的是,我知道 Windows 不是实时操作系统。我不希望任何给定的计时器间隔精确到超过 15 毫秒。然而,对我来说重要的是,我的计时器滴答声的总和从长远来看是准确的,这对于股票计时器或秒表来说是不可能的。

如果将普通的 System.Timers.Timer(或者更糟糕的是 Windows.Forms.Timer)配置为每 100 毫秒运行一次,则它会在 100 毫秒到大约 115 毫秒之间的某个时间滴答。这意味着,如果我需要在一天内完成该事件 10 * 60 * 60 * 24 = 864,000 次,我最终可能会迟到三个多小时!这对于我的应用程序来说是 Not Acceptable 。

如何最好地构造一个平均持续时间准确的计时器?我已经构建了一个,但我认为它可以更流畅、更有效,或者......必须有一个更好的方法。为什么.NET 提供低精度 DateTime.Now、低精度 Timer 和高精度 Diagnostics.Stopwatch,但没有高精度计时器?

我编写了以下计时器更新例程:

var CorrectiveStopwatch = new System.Diagnostics.Stopwatch();
var CorrectedTimer = new System.Timers.Timer()
{
    Interval = targetInterval,
    AutoReset = true,
};
CorrectedTimer.Elapsed += (o, e) =>
{
    var actualMilliseconds = ; 

    // Adjust the next tick so that it's accurate
    // EG: Stopwatch says we're at 2015 ms, we should be at 2000 ms
    // 2000 + 100 - 2015 = 85 and should trigger at the right time
    var newInterval = StopwatchCorrectedElapsedMilliseconds + 
                      targetInterval -
                       CorrectiveStopwatch.ElapsedMilliseconds;

    // If we're over 1 target interval too slow, trigger ASAP!
    if (newInterval <= 0)
    {
        newInterval = 1; 
    }

    CorrectedTimer.Interval = newInterval;

    StopwatchCorrectedElapsedMilliseconds += targetInterval;
};

正如您所看到的,它不太可能非常顺利。而且,在测试中,它一点也不顺利!对于一个示例运行,我得到以下值:

100
87
91
103
88
94
102
93
103
88

该序列的平均值为我了解网络时间和其他工具可以平滑地强制当前时间以获得准确的计时器。也许进行某种 PID 循环以使目标间隔达到 targetInterval - 15/2 左右可能需要较小的更新,或者平滑可能可以减少短事件和长事件之间的时序差异。我如何将其与我的方法结合起来?是否有现有的库可以做到这一点?

我将它与普通秒表进行了比较,它似乎很准确。 5 分钟后,我测量了以下毫秒计数器值:

DateTime elapsed:  300119
Stopwatch elapsed: 300131
Timer sum:         274800
Corrected sum:     300200

30 分钟后,我测量了:

DateTime elapsed:  1800208.2664
Stopwatch elapsed: 1800217
Timer sum:         1648500
Corrected sum:     1800300

库存计时器比秒表慢 151.8 秒,占总数的 8.4%。这似乎非常接近 100 到 115 毫秒之间的平均滴答持续时间!此外,校正后的总和仍然在测量值的 100 毫秒内(并且这些值比我用 watch 找到的误差更准确),所以这是有效的。

我的完整测试程序如下。它在 Windows 窗体中运行,需要“Form1”上有一个文本框,或者删除 textBox1.Text = 行并在命令行项目中运行它。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace StopwatchTimerTest
{
    public partial class Form1 : Form
    {
        private System.Diagnostics.Stopwatch ElapsedTimeStopwatch;
        private double TimerSumElapsedMilliseconds;
        private double StopwatchCorrectedElapsedMilliseconds;
        private DateTime StartTime;

        public Form1()
        {
            InitializeComponent();
            Run();
        }

        private void Run()
        {
            var targetInterval = 100;
            // A stopwatch running in normal mode provides the correct elapsed time to the best abilities of the system
            // (save, for example, a network time server or GPS peripheral)
            ElapsedTimeStopwatch = new System.Diagnostics.Stopwatch();

            // A normal System.Timers.Timer ticks somewhere between 100 ms and 115ms, quickly becoming inaccurate.
            var NormalTimer = new System.Timers.Timer()
            {
                Interval = targetInterval,
                AutoReset = true,
            };
            NormalTimer.Elapsed += (o, e) =>
            {
                TimerSumElapsedMilliseconds += NormalTimer.Interval;
            };

            // This is my "quick and dirty" corrective stopwatch
            var CorrectiveStopwatch = new System.Diagnostics.Stopwatch();
            var CorrectedTimer = new System.Timers.Timer()
            {
                Interval = targetInterval,
                AutoReset = true,
            };
            CorrectedTimer.Elapsed += (o, e) =>
            {
                var actualMilliseconds = CorrectiveStopwatch.ElapsedMilliseconds; // Drop this into a variable to avoid confusion in the debugger

                // Adjust the next tick so that it's accurate
                // EG: Stopwatch says we're at 2015 ms, we should be at 2000 ms, 2000 + 100 - 2015 = 85 and should trigger at the right time
                var newInterval = StopwatchCorrectedElapsedMilliseconds + targetInterval - actualMilliseconds;

                if (newInterval <= 0)
                {
                    newInterval = 1; // Basically trigger instantly in an attempt to catch up; could be smoother...
                }
                CorrectedTimer.Interval = newInterval;

                // Note: could be more accurate with actual interval, but actual app uses sum of tick events for many things
                StopwatchCorrectedElapsedMilliseconds += targetInterval;
            };

            var updateTimer = new System.Windows.Forms.Timer()
            {
                Interval = 1000,
                Enabled = true,
            };
            updateTimer.Tick += (o, e) =>
            {
                var result = "DateTime elapsed:  " + (DateTime.Now - StartTime).TotalMilliseconds.ToString() + Environment.NewLine + 
                             "Stopwatch elapsed: " + ElapsedTimeStopwatch.ElapsedMilliseconds.ToString() + Environment.NewLine +
                             "Timer sum:         " + TimerSumElapsedMilliseconds.ToString() + Environment.NewLine +
                             "Corrected sum:     " + StopwatchCorrectedElapsedMilliseconds.ToString();
                Console.WriteLine(result + Environment.NewLine);
                textBox1.Text = result;
            };

            // Start everything simultaneously
            StartTime = DateTime.Now;
            ElapsedTimeStopwatch.Start();
            updateTimer.Start();
            NormalTimer.Start();
            CorrectedTimer.Start();
            CorrectiveStopwatch.Start();
        }
    }
}

最佳答案

Windows is not a real-time OS

更准确地说,在保护模式按需分页虚拟内存操作系统上的环 3 中运行的代码不能假设它可以提供任何执行保证。像页面错误这样简单的事情就会极大地延迟代码执行。还有很多类似的事情,第二代垃圾收集总是会破坏你的期望。 Ring 0 代码(驱动程序)通常具有出色的中断响应时间,以在基准测试中作弊的蹩脚视频驱动程序为模。

所以不,希望计时器激活的代码能够立即运行通常是无用的希望。总会有延迟。 DispatcherTimer 或 Winforms Timer 只能在 UI 线程空闲时计时,异步计时器需要与所有其他线程池请求以及竞争处理器的其他进程拥有的线程竞争。

因此,您永远不想做的一件事就是在事件处理程序或回调中增加耗时计数器。它很快就会偏离挂钟时间。以一种完全不可预测的方式,机器负载越重,它就越落后。

解决方法很简单,如果您关心实际耗时,则使用时钟。无论是DateTime.UtcNow还是Environment.TickCount,两者没有根本区别。当您启动计时器时,将值存储在变量中,在事件或回调中减去该值即可知道实际耗时。它在长时间内具有高精度,精度受到时钟更新速率的限制。默认为每秒 64 次(15.6 毫秒),可以通过 pinvoking timeBeginPeriod() 来提升。 。这样做比较保守,对功耗不利。

秒表具有相反的特点,它的精度很高,但不精确。它没有校准到像 DateTime.UtcNow 和 Environment.TickCount 这样的原子钟。并且对于生成事件完全没有用,在循环中燃烧核心来检测耗时将使您的线程在烧完其量程后被放入狗屋一段时间。仅将其用于临时分析。

关于c# - 结合秒表和计时器,通过平滑强制获得长期准确的事件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33000567/

相关文章:

c# - 当 ItemsSource 更改时更新 Datagrid

javascript - Node.js setInterval 和 nextTick 第二次调用不准确

c - Pebble 秒表字体大小更改错误

c# - 在任务完成之前等待不阻塞

c# - 更改列的 IDENTITY 属性,需要删除并重新创建该列

c# - 调用值类型的方法

java - 捕获 2 个语句 Java 之间的执行时间?

c# - 尝试将字符串转换为 MarketDataIncrementalRefresh

java - 为我的 API 的每个用户创建一个线程是否可以很好地扩展?

c# - 运行速度超过 15 毫秒的秒表