在高分辨率间隔/计时器中触发事件

16
我想使用 C# 实现最高分辨率的计时器。例如,我希望在每11个tick(我听说tick是PC上最高的计数器)后引发一个事件。 我尝试了计时器,但发现最小的经过时间是毫秒级别的。 我看了看 Stopwatch,但它不会引发事件。
谢谢。

2
在世界上为什么你想要每11个tick引发一个事件?你是用这个来进行代码分析吗? - Cody Gray
5
您没有回答问题。11 tick只是一个例子。我需要的是高分辨率计时器。如果可能的话,最好能达到1 tick。是的,我正在开发一个应用程序来进行一些基准测试。 - Syaiful Nizam Yahya
3个回答

19

使用多媒体定时器应该能够每秒提供大约1000个事件。这段代码应该会对您有所帮助。

     public delegate void TimerEventHandler(UInt32 id, UInt32 msg, ref UInt32 userCtx, UInt32 rsv1, UInt32 rsv2);

    /// <summary>
    /// A multi media timer with millisecond precision
    /// </summary>
    /// <param name="msDelay">One event every msDelay milliseconds</param>
    /// <param name="msResolution">Timer precision indication (lower value is more precise but resource unfriendly)</param>
    /// <param name="handler">delegate to start</param>
    /// <param name="userCtx">callBack data </param>
    /// <param name="eventType">one event or multiple events</param>
    /// <remarks>Dont forget to call timeKillEvent!</remarks>
    /// <returns>0 on failure or any other value as a timer id to use for timeKillEvent</returns>
    [DllImport("winmm.dll", SetLastError = true,EntryPoint="timeSetEvent")]
    static extern UInt32 timeSetEvent(UInt32 msDelay, UInt32 msResolution, TimerEventHandler handler, ref UInt32 userCtx, UInt32 eventType);

    /// <summary>
    /// The multi media timer stop function
    /// </summary>
    /// <param name="uTimerID">timer id from timeSetEvent</param>
    /// <remarks>This function stops the timer</remarks>
    [DllImport("winmm.dll", SetLastError = true)]
    static extern void timeKillEvent(  UInt32 uTimerID );

运行完计时器后,请停止它们。它们对系统负担很重。请捕获所有异常并确保它们不会逃离您的事件处理程序。

*启动超过5个计时器会严重拖慢大多数系统!在事件处理程序中尽可能执行较少的代码,并确保执行的代码快于1毫秒,否则将面临严重问题。我每10-50次滴答声启动了一个委托以增加标签显示。

Thread.Sleep 上发生的普通线程切换将使得您的代码有一个线程槽位空闲,并且大约需要40毫秒。您也可以通过一些NT内核调用来增加线程切换频率,但请不要这样做。


14

首先,你需要明白由于硬件和软件的限制,在计算机上做精确的定时几乎是不可能的,甚至非常困难。好消息是,这种精度很少是必须的。十个滴答声是一个非常短暂的时间间隔,CPU在这段时间内完成的工作非常少,而且从统计学的角度来看,它永远不会有意义。

作为参考,Windows时钟的精度约为10毫秒(早期版本则更低)。用DateTime.UtcNow调用包装你的代码并不能达到比这更好的效果。

在你的问题中,你谈到想要“引发事件”。问题在于,唯一一种在特定时间间隔内引发事件的时间记录对象是Timer对象。它在.NET Framework中有三种不同的实现方式(System.Timers.TimerSystem.Threading.TimerSystem.Windows.Forms.Timer),它们都有自己独特的用途场景和相对应的怪癖,但它们都无法保证你所要求的精度。它们甚至没有被设计为这样做,也没有任何等效的Windows API函数能够提供这种类型的精度。

我问你为什么想这样做,以及你是否尝试进行基准测试的原因是因为这会改变整个游戏。 .NET Framework(截至2.0版本)提供了一个专门设计用于基准测试或性能分析等情况下准确测量经过时间的 Stopwatch 对象Stopwatch 简单地包装了 Windows API 函数 QueryPerformanceFrequencyQueryPerformanceCounter (这应该证实了我的建议)。 在 Framework 的早期版本中,我们曾经不得不 P/Invoke 这些函数才能访问此类功能,但现在已方便地内置在其中。 如果您需要具有相对较高分辨率的计时器进行基准测试,则 Stopwatch 是您的最佳选择。 理论上,它可以提供亚微秒级的计时。

但是它也存在问题。它不会引发任何事件,因此如果您当前的设计依赖于事件处理,您需要重新考虑。而且,它也不能保证完全准确。 当然,鉴于硬件限制,它可能具有最高分辨率,但这并不意味着它一定能满足您的要求。例如,在多处理器系统上,必须在同一个处理器上执行StartStop时,它可能不可靠。这应该无关紧要,但实际上却很重要。它还可能不可靠,因为处理器可以调节其时钟速度。更何况,即使在现代的2+ GHz处理器上,调用QueryPerformanceCounter本身也需要大约5微秒的时间,这导致您实际上无法达到理论上听起来很好的亚微秒级定时。不过,任何合理的代码分析器都会认为这个时间量是可以忽略的,因为,嗯,它确实可以被忽略
(另请参见:http://www.devsource.com/c/a/Techniques/High-Performance-Timing-under-Windows/2/


感谢您详细的解释。例如,如果我使用实时操作系统,是否可以获得那种高精度计时器?Windows Server是实时操作系统吗?谢谢。 - Syaiful Nizam Yahya
2
如果您使用RTOS,您可能有高分辨率计时器的机会。然而,实时操作系统并不意味着事情立即发生,而是在已知时间限制内发生,因此您仍然可能无法按照您想要的那样精确计时。至于Windows是否是RTOS,绝对不是。 - David Yaw
1
@publicENEMY:David Yaw的回答是正确的。即使是Windows Server版本也不是实时操作系统(我将Windows Server作为工作站运行,它并没有太大的区别)。值得指出的是,即使您克服了操作系统暴露的软件限制,仍然必须应对常见PC硬件的限制。如果您正在考虑切换操作系统,则必须有更好的方法来实现您要达到的目标。 - Cody Gray
1
Windows在它发货的盒子上从未标明RealTimeOS!.Net只会每40毫秒左右更新一次时间值。 - CodingBarfield
1
我看不出计时器如何有所帮助 - 它提供高分辨率的时间测量 - 但它并不以高分辨率触发事件。 - markmnl

4
各种计时器类使用更大的粒度。 Threading.Timer和Timers.Timer都使用1/64秒,即15.625毫秒。
如果你所说的“tick”是DateTime类、TimeSpan类使用的100纳秒tick,并由Stopwatch输出,则你提出的11 tick长度为1,100纳秒,或1.1微秒。据我所知,没有内置的计时器可以提供这样的分辨率。如果你确实想要每1.1微秒发生一次事件,你需要摒弃“计时器”的概念,而是考虑短暂延迟。将线程设置为高优先级,并在循环中运行你的事件。不要调用Thread.Sleep(),因为我认为1.1微秒比系统调度程序的时间片还要小。你需要使用延迟循环。
此外,请意识到你所询问的时间间隔非常非常小。在2 GHz处理器上,1.1微秒只有2,200个处理器周期。虽然不算少,但是完成大量工作的时间并不多。如果你所说的是1个tick,那只有200个处理器周期:这足以进行几十个数学运算,并可能调用一个函数。

我感兴趣的计时是 stopwatch.frequency 类型的计时。换句话说,就是处理器周期计时。 - Syaiful Nizam Yahya
3
处理器周期滴答声,例如2 GHz?那就是0.5纳秒。除了最简单的处理器指令外,所有操作都需要多个处理器周期。例如,你也许可以在一个周期内增加一个整数,但是一个"if"语句呢?不可能,那需要约10个处理器周期,即5纳秒。无法以这样的频率检查计时器,更不用说完成任何工作了。 - David Yaw
4
等等,我撤回刚才的话。有一种方法可以让计时器在1个处理器周期内完成:将线程设置为高优先级并执行while(true) { ... } - David Yaw
4
据我所知,while (true) 仍然会执行一个短跳转到循环的开始,这需要 1 个处理器周期,因为没有办法在少于 1 个处理器周期内执行指令。在汇编级别的编程中,if 语句的执行取决于你要进行比较的内容。与累加器比较是最快的 "if" 可能性(CMP EAX)。比较本身需要 1 或 2 个周期(如果我没记错的话),然后需要另一个周期进行短跳转... 因此,运气不佳。要求绝对不可能就是... 不可能的事情。 - ThunderGr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接