.NET中最准确的计时器?

时间:2012-02-10 13:11:13

标签: c# timer

运行以下(略微伪)代码会产生以下结果。我对计时器的真空程度感到震惊(每个Tick增加~14ms)。

那里有更准确的东西吗?

void Main()
{
   var timer = new System.Threading.Timer(TimerCallback, null, 0, 1000);
}

void TimerCallback(object state)
{
   Debug.WriteLine(DateTime.Now.ToString("ss.ffff"));
}

Sample Output:
...
11.9109
12.9190
13.9331
14.9491
15.9632
16.9752
17.9893
19.0043
20.0164
21.0305
22.0445
23.0586
24.0726
25.0867
26.1008
27.1148
28.1289
29.1429
30.1570
31.1710
32.1851

11 个答案:

答案 0 :(得分:19)

要准确测量时间,您需要使用秒表课程 MSDN

答案 1 :(得分:12)

我还有一个精确到1ms的课程。我从论坛上拿了Hans Passant的代码  https://social.msdn.microsoft.com/Forums/en-US/6cd5d9e3-e01a-49c4-9976-6c6a2f16ad57/1-millisecond-timer
并将其包装在一个类中,以便在您的表单中使用。如果需要,您可以轻松设置多个计时器。在下面的示例代码中,我使用了2个计时器。我测试了它,它工作正常。

// AccurateTimer.cs
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace YourProjectsNamespace
{
    class AccurateTimer
    {
        private delegate void TimerEventDel(int id, int msg, IntPtr user, int dw1, int dw2);
        private const int TIME_PERIODIC = 1;
        private const int EVENT_TYPE = TIME_PERIODIC;// + 0x100;  // TIME_KILL_SYNCHRONOUS causes a hang ?!
        [DllImport("winmm.dll")]
        private static extern int timeBeginPeriod(int msec);
        [DllImport("winmm.dll")]
        private static extern int timeEndPeriod(int msec);
        [DllImport("winmm.dll")]
        private static extern int timeSetEvent(int delay, int resolution, TimerEventDel handler, IntPtr user, int eventType);
        [DllImport("winmm.dll")]
        private static extern int timeKillEvent(int id);

        Action mAction;
        Form mForm;
        private int mTimerId;
        private TimerEventDel mHandler;  // NOTE: declare at class scope so garbage collector doesn't release it!!!

        public AccurateTimer(Form form,Action action,int delay)
        {
            mAction = action;
            mForm = form;
            timeBeginPeriod(1);
            mHandler = new TimerEventDel(TimerCallback);
            mTimerId = timeSetEvent(delay, 0, mHandler, IntPtr.Zero, EVENT_TYPE);
        }

        public void Stop()
        {
            int err = timeKillEvent(mTimerId);
            timeEndPeriod(1);
            System.Threading.Thread.Sleep(100);// Ensure callbacks are drained
        }

        private void TimerCallback(int id, int msg, IntPtr user, int dw1, int dw2)
        {
            if (mTimerId != 0)
                mForm.BeginInvoke(mAction);
        }
    }
}

// FormMain.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace YourProjectsNamespace
{
    public partial class FormMain : Form
    {
        AccurateTimer mTimer1,mTimer2;

        public FormMain()
        {
            InitializeComponent();
        }

        private void FormMain_Load(object sender, EventArgs e)
        {
            int delay = 10;   // In milliseconds. 10 = 1/100th second.
            mTimer1 = new AccurateTimer(this, new Action(TimerTick1),delay);
            delay = 100;      // 100 = 1/10th second.
            mTimer2 = new AccurateTimer(this, new Action(TimerTick2), delay);
        }

        private void FormMain_FormClosing(object sender, FormClosingEventArgs e)
        {
            mTimer1.Stop();
            mTimer2.Stop();
        }

        private void TimerTick1()
        {
            // Put your first timer code here!
        }

        private void TimerTick2()
        {
            // Put your second timer code here!
        }
    }
}

答案 2 :(得分:10)

我认为其他答案未能解决为什么 14ms在OP代码的每次迭代中都会出现这种情况;因为系统时钟不精确而导致(并且DateTime.Now不准确,除非您已关闭NTP服务或设置了错误的时区或傻!它只是不精确)。

准确的计时器

即使有一个不精确的系统时钟(利用DateTime.Now,或者将一个太阳能电池连接到ADC来判断太阳在天空中有多高,或者在峰值潮汐之间划分时间,或者。 ..),遵循这种模式的代码将具有平均零转换(它将完全准确,平均每个刻度之间只有一秒):

var interval = new TimeSpan(0, 0, 1);
var nextTick = DateTime.Now + interval;
while (true)
{
    while ( DateTime.Now < nextTick )
    {
        Thread.Sleep( nextTick - DateTime.Now );
    }
    nextTick += interval; // Notice we're adding onto when the last tick was supposed to be, not when it is now
    // Insert tick() code here
}

(如果您正在复制并粘贴此内容,请注意您的刻度代码执行时间超过interval的情况。我会将其作为练习留给读者找到简单的方法使这个跳过尽可能多的节拍nextTick将来降落

不准确的计时器

我猜测Microsoft的System.Threading.Timer实现遵循这种模式。即使只使用完美精确且完全精确的系统计时器,这种模式也总是可以摆动(因为即使只是添加操作也需要时间):

var interval = new TimeSpan(0, 0, 1);
var nextTick = DateTime.Now + interval;
while (true)
{
    while ( DateTime.Now < nextTick )
    {
        Thread.Sleep( nextTick - DateTime.Now );
    }
    nextTick = DateTime.Now + interval; // Notice we're adding onto .Now instead of when the last tick was supposed to be. This is where slew comes from
    // Insert tick() code here
}

因此,对于那些可能有兴趣推出自己的计时器的人来说,不要遵循第二种模式。

精确时间测量

正如其他海报所说,Stopwatch类为时间测量提供了很好的精度,但如果是精确度则根本不提供帮助遵循错误的模式。但是,as @Shahar said它并不像你一样会开始得到一个完美精确的计时器,所以如果完美精确是什么你需要重新思考一下你之后。

免责声明

请注意,微软并没有谈论System.Threading.Timer课程的内部情况,所以我在教育上对此进行了推测,但如果它像鸭子一样嘎嘎叫,那么它可能是鸭。此外,我意识到这已经有好几年了,但它仍然是一个相关的(我认为没有答案)问题。

编辑:更改了@ Shahar回答的链接

编辑:微软有很多在线内容的源代码,包括System.Threading.Timer,对于那些有兴趣了解微软如何实现该计时器的人来说

答案 3 :(得分:4)

Timer和DateTime没有足够的准确性用于您的目的。请尝试使用Stopwatch。 请查看以下文章以获取更多详细信息:

http://blogs.msdn.com/b/ericlippert/archive/2010/04/08/precision-and-accuracy-of-datetime.aspx

答案 4 :(得分:4)

桌面操作系统(如Windows)实时操作系统。这意味着,您无法期望完全准确,并且您无法强制调度程序以您想要的精确毫秒触发代码。特别是在.NET应用程序中,这是非确定性的......例如,只要GC可以开始收集,JIT编译可能会慢一些或者更快一些....

答案 5 :(得分:3)

它不是不准确的计时器,而是DateTime.Now,其广告容差为16毫秒。

相反,我会使用Environment.Ticks属性来测量此测试期间的CPU周期。

编辑:Environment.Ticks也基于系统计时器,可能与DateTime.Now具有相同的准确性问题。我建议选择StopWatch,而不是像许多其他回答者所提到的那样。

答案 6 :(得分:2)

记录下来,今天看来这是固定的。

使用OPs代码,我在.NET Core 3.1中得到了这个代码:

41.4263
42.4263
43.4291
44.4262
45.4261
46.4261
47.4261
48.4261
49.4260
50.4260
51.4260
52.4261

答案 7 :(得分:2)

几年后,但我想到了here。它本身会发热,通常精确到1ms以下。简而言之,它从占用大量CPU的Task.Delay开始,然后向上进行一次spinwait。通常精确到大约50µs(0.05 ms)。

static void Main()
{
    PrecisionRepeatActionOnIntervalAsync(SayHello(), TimeSpan.FromMilliseconds(1000)).Wait();
}

// Some Function
public static Action SayHello() => () => Console.WriteLine(DateTime.Now.ToString("ss.ffff"));
        

public static async Task PrecisionRepeatActionOnIntervalAsync(Action action, TimeSpan interval, CancellationToken? ct = null)
{
    long stage1Delay = 20 ;
    long stage2Delay = 5 * TimeSpan.TicksPerMillisecond;
    bool USE_SLEEP0 = false;

    DateTime target = DateTime.Now + new TimeSpan(0, 0, 0, 0, (int)stage1Delay + 2);
    bool warmup = true;
    while (true)
    {
        // Getting closer to 'target' - Lets do the less precise but least cpu intesive wait
        var timeLeft = target - DateTime.Now;
        if (timeLeft.TotalMilliseconds >= stage1Delay)
        {
            try
            {
                await Task.Delay((int)(timeLeft.TotalMilliseconds - stage1Delay), ct ?? CancellationToken.None);
            }
            catch (TaskCanceledException) when (ct != null)
            {
                return;
            }
        }

        // Getting closer to 'target' - Lets do the semi-precise but mild cpu intesive wait - Task.Yield()
        while (DateTime.Now < target - new TimeSpan(stage2Delay))
        {
            await Task.Yield();
        }

        // Getting closer to 'target' - Lets do the semi-precise but mild cpu intensive wait - Thread.Sleep(0)
        // Note: Thread.Sleep(0) is removed below because it is sometimes looked down on and also said not good to mix 'Thread.Sleep(0)' with Tasks.
        //       However, Thread.Sleep(0) does have a quicker and more reliable turn around time then Task.Yield() so to 
        //       make up for this a longer (and more expensive) Thread.SpinWait(1) would be needed.
        if (USE_SLEEP0)
        {
            while (DateTime.Now < target - new TimeSpan(stage2Delay / 8))
            {
                Thread.Sleep(0);
            }
        }

        // Extreamlly close to 'target' - Lets do the most precise but very cpu/battery intesive 
        while (DateTime.Now < target)
        {
            Thread.SpinWait(64);
        }

        if (!warmup)
        {
            await Task.Run(action); // or your code here
            target += interval;
        }
        else
        {
            long start1 = DateTime.Now.Ticks + ((long)interval.TotalMilliseconds * TimeSpan.TicksPerMillisecond);
            long alignVal = start1 - (start1 % ((long)interval.TotalMilliseconds * TimeSpan.TicksPerMillisecond));
            target = new DateTime(alignVal);
            warmup = false;
        }
    }
}


Sample output:
07.0000
08.0000
09.0000
10.0001
11.0000
12.0001
13.0000
14.0000
15.0000
16.0000
17.0000
18.0000
19.0001
20.0000
21.0000
22.0000
23.0000
24.0000
25.0000
26.0000
27.0000
28.0000
29.0000
30.0000
31.0000
32.0138 <---not that common but can happen
33.0000
34.0000
35.0001
36.0000
37.0000
38.0000
39.0000
40.0000
41.0000

答案 8 :(得分:0)

这是另一种方法。在我的机器上精确到5-20毫秒。

public class Run
{
    public Timer timer;

    public Run()
    {
        var nextSecond = MilliUntilNextSecond();

        var timerTracker = new TimerTracker()
        {
            StartDate = DateTime.Now.AddMilliseconds(nextSecond),
            Interval = 1000,
            Number = 0
        };

        timer = new Timer(TimerCallback, timerTracker, nextSecond, -1);
    }

    public class TimerTracker
    {
        public DateTime StartDate;
        public int Interval;
        public int Number;
    }

    void TimerCallback(object state)
    {
        var timeTracker = (TimerTracker)state;
        timeTracker.Number += 1;
        var targetDate = timeTracker.StartDate.AddMilliseconds(timeTracker.Number * timeTracker.Interval);
        var milliDouble = Math.Max((targetDate - DateTime.Now).TotalMilliseconds, 0);
        var milliInt = Convert.ToInt32(milliDouble);
        timer.Change(milliInt, -1);

        Console.WriteLine(DateTime.Now.ToString("ss.fff"));
    }

    public static int MilliUntilNextSecond()
    {
        var time = DateTime.Now.TimeOfDay;
        var shortTime = new TimeSpan(0, time.Hours, time.Minutes, time.Seconds, 0);
        var oneSec = new TimeSpan(0, 0, 1);
        var milliDouble = (shortTime.Add(oneSec) - time).TotalMilliseconds;
        var milliInt = Convert.ToInt32(milliDouble);
        return milliInt;
    }
}

答案 9 :(得分:0)

这并不能真正使计时器更准确(如并不能确保两次回调之间的时间恰好是1秒),但是如果您需要的只是一个可以触发一次的计时器由于~14ms漂移问题而导致每秒跳动,并且不会跳过秒(如OP在第17到19秒之间的示例输出所示),您只需更改计时器即可在即将到来的第二秒开始时触发随着回调触发(显然,您可以在接下来的分钟,接下来的小时等时间内执行相同的操作,如果您关心的只是确保时间间隔不漂移):

using System.Threading;

static Timer timer;

void Main()
{   
    // 1000 - DateTime.UtcNow.Millisecond = number of milliseconds until the next second
    timer = new Timer(TimerCallback, null, 1000 - DateTime.UtcNow.Millisecond, 0);
}

void TimerCallback(object state)
{   
    // Important to do this before you do anything else in the callback
    timer.Change(1000 - DateTime.UtcNow.Millisecond, 0);

    Debug.WriteLine(DateTime.UtcNow.ToString("ss.ffff"));
}

Sample Output:
...
25.0135
26.0111
27.0134
28.0131
29.0117
30.0135
31.0127
32.0104
33.0158
34.0113
35.0129
36.0117
37.0127
38.0101
39.0125
40.0108
41.0156
42.0110
43.0141
44.0100
45.0149
46.0110
47.0127
48.0109
49.0156
50.0096
51.0166
52.0009
53.0111
54.0126
55.0116
56.0128
57.0110
58.0129
59.0120
00.0106
01.0149
02.0107
03.0136

答案 10 :(得分:-4)

我为此做了一堂课,似乎工作得很好。没有任何不准确之处:

class AccurateTimer
{

    public event EventHandler<EventArgs> Tick;

    public bool Running { get; private set; }
    public int Interval { get; private set; }

    public AccurateTimer(int interval_ = 1000)
    {
        Running = false;
        Interval = interval_;
    }

    public void Start()
    {
        Running = true;
        Thread thread = new Thread(Run);
        thread.Start();
    }

    public void Stop()
    {
        Running = false;
    }

    private void Run()
    {
        DateTime nextTick = DateTime.Now.AddMilliseconds(Interval);
        while (Running)
        {
            if (DateTime.Now > nextTick)
            {
                nextTick = nextTick.AddMilliseconds(Interval);
                OnTick(EventArgs.Empty);
            }
        }
    }

    protected void OnTick(EventArgs e)
    {
        EventHandler<EventArgs> copy = Tick;
        if (copy != null)
        {
            copy(this, e);
        }
    }

}

但它可能不是最佳解决方案。