结合秒表和计时器,以获得平稳强制的长期准确事件

时间:2015-10-07 19:12:41

标签: c# timer stopwatch

首先,我要说我明白Windows不是实时操作系统。我不认为任何给定的定时器间隔精确到15 ms以上。但是,对我来说很重要的是,我的计时器滴答的总和在长期内是准确的,这对于库存计时器或秒表是不可能的。

如果将正常的System.Timers.Timer(或更糟糕的是,Windows.Forms.Timer)配置为每100毫秒运行一次,则它会在100毫秒到大约115毫秒之间的某个时间点。这意味着如果我需要做一天的活动,10 * 60 * 60 * 24 = 864,000次,我最终可能会超过三个小时!这对我的申请来说是不可接受的。

如何才能最好地构建平均持续时间准确的计时器?我已经建了一个,但我认为它可以更顺畅,更有效,或者......只需要有更好的方法。为什么.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毫秒内(这些值比我手表上的误差更准确),所以这样做。

我的完整测试程序如下。它在Windows窗体中运行,需要在&#34; Form1&#34;上的文本框,或者删除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();
        }
    }
}

1 个答案:

答案 0 :(得分:2)

  

Windows不是实时操作系统

更准确地说,在受保护模式请求分页虚拟内存操作系统上的环3中运行的代码不能假设它可以提供任何执行保证。像页面错误这样简单的东西会大大延迟代码执行。更多的是,第二代垃圾收集总是会破坏你的期望。 Ring 0代码(驱动程序)通常具有出色的中断响应时间,模块化了一个在其基准测试中作弊的糟糕视频驱动程序。

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

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

解决方法很简单,如果您关心实际经过的时间,那么请使用时钟。无论是DateTime.UtcNow还是Environment.TickCount,两者之间都没有根本的区别。启动计时器时将值存储在变量中,在事件或回调中减去它以了解实际经过的时间。它在长时间内非常精确,精度受时钟更新速率的限制。默认情况下,每秒64次(15.6毫秒),可以通过timeBeginPeriod()进行加扰。保守地这样做,这对于功耗是不利的。

秒表具有相反的特征,它非常准确但不精确。它没有校准到DateTime.UtcNow和Environment.TickCount等原子钟。生成事件是完全没用的,在循环中刻录核心以检测经过的时间会让你的线程在烧掉它的量子之后放入狗屋一段时间。仅用于临时分析。