.NET 中最准确的计时器?

发布于 2025-01-04 21:38:31 字数 482 浏览 0 评论 0 原文

运行以下(稍微伪)代码会产生以下结果。我对计时器的不准确程度感到震惊(每个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

Running the following (slightly pseudo)code produces the following results. Im shocked at how innacurate the timer is (gains ~14ms each Tick).

Is there anything more accurate out there?

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

标点 2025-01-11 21:38:31

我也上过一堂课,精确到1ms。我从论坛获取了 Hans Passant 的代码
<一href="https://social.msdn.microsoft.com/Forums/en-US/6cd5d9e3-e01a-49c4-9976-6c6a2f16ad57/1-millisecond-timer"> 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!
        }
    }
}

I also have witten a class which is accurate to 1ms. I took Hans Passant's code from forum
https://social.msdn.microsoft.com/Forums/en-US/6cd5d9e3-e01a-49c4-9976-6c6a2f16ad57/1-millisecond-timer
and wrapped it in a class for ease of use in your Form. You can easily set up multiple timers if you want. In the example code below I have used 2 timers. I have tested it and it works ok.

// 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!
        }
    }
}
始终不够爱げ你 2025-01-11 21:38:31

我认为其他答案未能解决为什么OP代码的每次迭代需要14ms的时间;这不是,因为系统时钟不精确(并且 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
}

如果您复制并粘贴此内容,请注意情况下你的tick 代码的执行时间比 interval 更长,我将把它作为练习,让读者找到简单的方法来使其跳过 nextTick 所需的节拍数。 > 降落在未来)

不准确的计时器

我猜测微软的 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 类为时间测量提供了很好的精度,但对于准确性没有任何帮助,如果遵循了错误的模式。但是,正如 @Shahar 所说,你不可能一开始就得到一个完全精确的计时器,所以如果您追求的是完美的精度,您就需要重新思考。

免责声明

请注意,Microsoft 并没有过多谈论 的内部结构System.Threading.Timer 类,所以我有根据地推测它,但如果它像鸭子一样嘎嘎叫,那么它可能是一只鸭子。另外,我意识到这已经有好几年了,但它仍然是一个相关的(我认为尚未回答的)问题。

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

编辑:微软在网上有很多东西的源代码,包括 System.Threading.Timer,适合任何有兴趣了解 Microsoft 如何实现该 slew-y 计时器的人。

I think the other answers are failing to address why there's 14ms slew through each iteration of the OP's code; it's not because of an imprecise system clock (and DateTime.Now is not inaccurate, unless you've turned off NTP services or have the wrong time zone set or something silly! It's only imprecise).

Accurate timer

Even with an imprecise system clock (making use of DateTime.Now, or having a solar cell hooked up to an ADC to tell how high the sun is in the sky, or dividing the time between peak tides, or ...), code following this pattern will have an average of zero slew (it will be perfectly accurate with exactly one second between ticks on average):

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
}

(If you're copying-and-pasting this, watch out for cases where your tick code takes longer than interval to execute. I'll leave it as an exercise for the reader to find the easy ways to make this skip as many beats as it takes for nextTick to land in the future)

Inaccurate timer

I'm guessing that Microsoft's implementation of System.Threading.Timer follows this kind of pattern instead. This pattern will always have slew even with a perfectly precise and perfectly accurate system timer (because it takes time to execute even just the add operation):

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
}

So for folks who might be interested in rolling your own timer, don't follow this second pattern.

Precise time measurement

As other posters have said, the Stopwatch class gives great precision for time measurement, but doesn't help at all with accuracy if the wrong pattern is followed. But, as @Shahar said it's not like you're ever going to get a perfectly-precise timer to begin with, so you need to rethink things if perfect precision is what you're after.

Disclaimers

Note that Microsoft doesn't talk much about the internals of the System.Threading.Timer class so I'm educatedly speculating about it, but if it quacks like a duck then it's probably a duck. Also, I realize this is several years old, but it's still a relevant (and I think unanswered) question.

Edit: Changed link to @Shahar's answer

Edit: Microsoft has source code for a lot of stuff online, including System.Threading.Timer, for anyone who is interested in seeing how Microsoft implemented that slew-y timer.

人海汹涌 2025-01-11 21:38:31

几年后,但是这里< /a> 是我想出的。它会自行对齐,并且通常精确到 1 毫秒以下。简而言之,它从低 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.UtcNow.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.UtcNow + 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.UtcNow;
        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.UtcNow < 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.UtcNow < target - new TimeSpan(stage2Delay / 8))
            {
                Thread.Sleep(0);
            }
        }

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

        if (!warmup)
        {
            await Task.Run(action); // or your code here
            target += interval;
        }
        else
        {
            long start1 = DateTime.UtcNow.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

Some years later but here is what I came up with. It alines itself and is usually accurate to under 1ms. In a nutshell, it starts with a low CPU intensive Task.Delay and moves up to a spinwait. it is usually accurate to about 50µs (0.05 ms).

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

// Some Function
public static Action SayHello() => () => Console.WriteLine(DateTime.UtcNow.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.UtcNow + 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.UtcNow;
        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.UtcNow < 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.UtcNow < target - new TimeSpan(stage2Delay / 8))
            {
                Thread.Sleep(0);
            }
        }

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

        if (!warmup)
        {
            await Task.Run(action); // or your code here
            target += interval;
        }
        else
        {
            long start1 = DateTime.UtcNow.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
信仰 2025-01-11 21:38:31

为了精确测量时间,您需要使用 Stopwatch 类
MSDN

For exact time measuring you need to use the Stopwatch class
MSDN

放赐 2025-01-11 21:38:31

根据记录,这个问题现在似乎已得到解决。

通过 OP 代码,我在 .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

For the record, this seems to be fixed nowadays.

With OPs code, I get this in .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
沉鱼一梦 2025-01-11 21:38:31

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

Desktop operating system (such as windows) are not real-time operating system. which means, you can't expect full accuracy and you can't force the scheduler to trigger your code in the exact millisecond you want. Specially in .NET application which is non-deterministic...for examply, any time the GC can start collecting, a JIT compilation might be a bit slower or a bit faster....

会傲 2025-01-11 21:38:31

Timer 和 DateTime 的精度不足以满足您的目的。请尝试使用秒表
请参阅以下文章了解更多详细信息:

https://learn.microsoft.com/en-us/archive/blogs/ericlippert/ precision-and-accuracy-of-datetime

Timer and DateTime do not have enough accuracy for your purpose. Try the Stopwatch instead.
Look at the following article for more details:

https://learn.microsoft.com/en-us/archive/blogs/ericlippert/precision-and-accuracy-of-datetime

涙—继续流 2025-01-11 21:38:31

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

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

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

Its not the timer that is inaccurate, but DateTime.Now, which has an advertised tolerance of 16ms.

Instead I would use the Environment.Ticks property to measure the CPU cycles during this test.

Edit: Environment.Ticks is also based off the system timer and may have the same accuracy issues as DateTime.Now. I'd advise choosing the StopWatch instead as many other answerers have mentioned.

爱冒险 2025-01-11 21:38:31

这并不能真正使计时器更加准确(如 它不能确保回调之间的时间恰好是 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

This doesn't really make the timer more accurate (as in it doesn't make sure the time between callbacks is exactly 1 second), but if all you need is a timer which fires once every second and doesn't skip seconds because of the ~14ms drifting issue (as demonstrated in OP's sample output between the 17th and 19th second), you could simply change the timer to fire at the start of the upcoming second as soon as the callback fires (and obviously you could do the same for upcoming minute, upcoming hour and so on, if all you care about is making sure the interval doesn't drift):

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
随遇而安 2025-01-11 21:38:31

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

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;
    }
}

Here is another approach. Accurate to within 5-20ms on my machine.

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;
    }
}
方圜几里 2025-01-11 21:38:31

我为此开设了一个课程,看起来效果很好。没有任何不准确之处:

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);
        }
    }

}

但这可能不是最好的解决方案。

I've made a class for that, and it seems to be working just fine. No inaccuracy whatsoever:

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);
        }
    }

}

It might not be the best solution, though.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文