.NET中最准确的计时器?

max*_*axp 30 c# timer

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

那里有更准确的东西吗?

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
Run Code Online (Sandbox Code Playgroud)

RA.*_*RA. 18

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

  • 为什么这标记为答案?我相信这个问题要求更精确的TIMER不是一个准确的测量工具.他的测量结果非常好 - 无论你如何测量,计时器确实会在每次回调后最多触发15毫秒. (20认同)

小智 14

我也有一个精确到1ms的课程.我从论坛
https://social.msdn.microsoft.com/Forums/en-US/6cd5d9e3-e01a-49c4-9976-6c6a2f16ad57/1-millisecond-timer中获取 Hans Passant的代码
并将其包装在一个易于使用的类中在你的表格中.如果需要,您可以轻松设置多个计时器.在下面的示例代码中,我使用了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!
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Sun*_*est 13

几年后,但这就是我的想法。它会自行对齐,并且通常精确到 1 毫秒以下。简而言之,它从低 CPU 密集型 Task.Delay 开始,然后向上移动到 spinwait。它通常精确到大约 50\xc2\xb5s (0.05 ms)。

\n
static void Main()\n{\n    PrecisionRepeatActionOnIntervalAsync(SayHello(), TimeSpan.FromMilliseconds(1000)).Wait();\n}\n\n// Some Function\npublic static Action SayHello() => () => Console.WriteLine(DateTime.Now.ToString("ss.ffff"));\n        \n\npublic static async Task PrecisionRepeatActionOnIntervalAsync(Action action, TimeSpan interval, CancellationToken? ct = null)\n{\n    long stage1Delay = 20 ;\n    long stage2Delay = 5 * TimeSpan.TicksPerMillisecond;\n    bool USE_SLEEP0 = false;\n\n    DateTime target = DateTime.Now + new TimeSpan(0, 0, 0, 0, (int)stage1Delay + 2);\n    bool warmup = true;\n    while (true)\n    {\n        // Getting closer to \'target\' - Lets do the less precise but least cpu intesive wait\n        var timeLeft = target - DateTime.Now;\n        if (timeLeft.TotalMilliseconds >= stage1Delay)\n        {\n            try\n            {\n                await Task.Delay((int)(timeLeft.TotalMilliseconds - stage1Delay), ct ?? CancellationToken.None);\n            }\n            catch (TaskCanceledException) when (ct != null)\n            {\n                return;\n            }\n        }\n\n        // Getting closer to \'target\' - Lets do the semi-precise but mild cpu intesive wait - Task.Yield()\n        while (DateTime.Now < target - new TimeSpan(stage2Delay))\n        {\n            await Task.Yield();\n        }\n\n        // Getting closer to \'target\' - Lets do the semi-precise but mild cpu intensive wait - Thread.Sleep(0)\n        // 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.\n        //       However, Thread.Sleep(0) does have a quicker and more reliable turn around time then Task.Yield() so to \n        //       make up for this a longer (and more expensive) Thread.SpinWait(1) would be needed.\n        if (USE_SLEEP0)\n        {\n            while (DateTime.Now < target - new TimeSpan(stage2Delay / 8))\n            {\n                Thread.Sleep(0);\n            }\n        }\n\n        // Extreamlly close to \'target\' - Lets do the most precise but very cpu/battery intesive \n        while (DateTime.Now < target)\n        {\n            Thread.SpinWait(64);\n        }\n\n        if (!warmup)\n        {\n            await Task.Run(action); // or your code here\n            target += interval;\n        }\n        else\n        {\n            long start1 = DateTime.Now.Ticks + ((long)interval.TotalMilliseconds * TimeSpan.TicksPerMillisecond);\n            long alignVal = start1 - (start1 % ((long)interval.TotalMilliseconds * TimeSpan.TicksPerMillisecond));\n            target = new DateTime(alignVal);\n            warmup = false;\n        }\n    }\n}\n\n\nSample output:\n07.0000\n08.0000\n09.0000\n10.0001\n11.0000\n12.0001\n13.0000\n14.0000\n15.0000\n16.0000\n17.0000\n18.0000\n19.0001\n20.0000\n21.0000\n22.0000\n23.0000\n24.0000\n25.0000\n26.0000\n27.0000\n28.0000\n29.0000\n30.0000\n31.0000\n32.0138 <---not that common but can happen\n33.0000\n34.0000\n35.0001\n36.0000\n37.0000\n38.0000\n39.0000\n40.0000\n41.0000\n
Run Code Online (Sandbox Code Playgroud)\n


Mat*_*mas 11

我认为其他答案未能解决为什么在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
}
Run Code Online (Sandbox Code Playgroud)

(如果你正在复制并粘贴它,请注意你的刻度代码需要更长时间而不是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
}
Run Code Online (Sandbox Code Playgroud)

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

精确的时间测量

正如其他海报所说,该Stopwatch课程为时间测量提供了极高的精确度,但如果遵循错误的模式,则完全没有准确性.但是,正如@Shahar说的那样,你不会想要一个完美精确的计时器开始,所以你需要重新思考,如果完美的精确度是你所追求的.

免责声明

需要注意的是微软并没有过多谈论的的内部System.Threading.Timer类,所以我educatedly猜测,但如果它叫起来像鸭子那么它可能就是一只鸭子.此外,我意识到这已经有好几年了,但它仍然是一个相关的(我认为没有答案)问题.

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

编辑:微软拥有许多在线资源的源代码,包括System.Threading.Timer,适用于有兴趣了解微软如何实现该计时器的人

  • 在 **精确计时器** 示例中,如果间隔小于 Windows 默认计时器周期(即 15.625 毫秒或 64 个刻度/秒),则 Sleep() 的 timespan 参数有可能为负值,并且会抛出异常(或者更糟糕的是,无限期地休眠,因为 -1 = Timeout.infinite) (3认同)
  • 我以@HerryYT 的评论[编辑](https://stackoverflow.com/revisions/34661516/6) 的形式发布:注意:带有“x &gt; 1”的“Thread.Sleep(x)”可能会增加一些额外的延迟由于其任务,除了不断使用 CPU 核心之外。要解决此问题,您可能需要使用“Thread.Sleep(0);”(这将减少延迟,因为不涉及上下文切换并释放 CPU 时间)。 (2认同)

Eik*_*ike 7

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

通过 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
Run Code Online (Sandbox Code Playgroud)

  • 另请注意,您可以使用 [AppContextSwitchOverrides](https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/appcontextswitchoverrides-) 在 Framework 项目中启用新计时器元素) 。在您的 app.config 中或以编程方式 `AppContext.SetSwitch("Switch.System.Threading.UseNetCoreTimer", true);` (2认同)