【发布时间】:2010-09-19 00:12:40
【问题描述】:
请注意,我问的是使用System.Threading.Timer 之类的东西每15 毫秒调用一次回调函数的频率。我不是在问如何使用System.Diagnostics.Stopwatch 甚至QueryPerformanceCounter 之类的东西准确地计时一段代码。
另外,我已经阅读了相关问题:
Accurate Windows timer? System.Timers.Timer() is limited to 15 msec
这两个都没有为我的问题提供有用的答案。
此外,推荐的 MSDN 文章 Implement a Continuously Updating, High-Resolution Time Provider for Windows 是关于计时而不是提供连续的滴答声。
话虽如此。 . .
关于 .NET 计时器对象有很多不好的信息。例如,System.Timers.Timer 被称为“针对服务器应用程序优化的高性能计时器”。而System.Threading.Timer 在某种程度上被认为是二等公民。传统观点认为System.Threading.Timer 是Windows Timer Queue Timers 的包装,而System.Timers.Timer 则完全不同。
现实情况大不相同。 System.Timers.Timer 只是 System.Threading.Timer 周围的一个薄组件包装器(只需使用 Reflector 或 ILDASM 窥视 System.Timers.Timer 内部,你会看到对 System.Threading.Timer 的引用),并且有一些代码可以提供自动线程同步,所以你不必这样做。
System.Threading.Timer,事实证明不是 Timer Queue Timers 的包装器。至少在 .NET 2.0 到 .NET 3.5 使用的 2.0 运行时中没有。使用共享源 CLI 的几分钟表明,运行时实现了自己的定时器队列,类似于 Timer Queue Timers,但从未真正调用 Win32 函数。
.NET 4.0 运行时似乎也实现了自己的计时器队列。我的测试程序(见下文)在 .NET 4.0 下提供了与在 .NET 3.5 下类似的结果。我已经为 Timer Queue Timers 创建了自己的托管包装器,并证明我可以获得 1 毫秒的分辨率(具有相当高的准确性),所以我认为我不太可能读错 CLI 源代码。
我有两个问题:
首先,是什么导致运行时对定时器队列的实现如此缓慢?我无法获得超过 15 毫秒的分辨率,准确度似乎在 -1 到 +30 毫秒的范围内。也就是说,如果我要求 24 毫秒,我会得到相隔 23 到 54 毫秒的滴答声。我想我可以花更多时间使用 CLI 源来寻找答案,但我想这里有人可能知道。
其次,我意识到这很难回答,为什么不使用 Timer Queue Timers?我意识到 .NET 1.x 必须在没有这些 API 的 Win9x 上运行,但它们自 Windows 2000 以来就已经存在,如果我没记错的话,这是 .NET 2.0 的最低要求。是因为 CLI 必须在非 Windows 机器上运行吗?
我的计时器测试程序:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace TimerTest
{
class Program
{
const int TickFrequency = 5;
const int TestDuration = 15000; // 15 seconds
static void Main(string[] args)
{
// Create a list to hold the tick times
// The list is pre-allocated to prevent list resizing
// from slowing down the test.
List<double> tickTimes = new List<double>(2 * TestDuration / TickFrequency);
// Start a stopwatch so we can keep track of how long this takes.
Stopwatch Elapsed = Stopwatch.StartNew();
// Create a timer that saves the elapsed time at each tick
Timer ticker = new Timer((s) =>
{
tickTimes.Add(Elapsed.ElapsedMilliseconds);
}, null, 0, TickFrequency);
// Wait for the test to complete
Thread.Sleep(TestDuration);
// Destroy the timer and stop the stopwatch
ticker.Dispose();
Elapsed.Stop();
// Now let's analyze the results
Console.WriteLine("{0:N0} ticks in {1:N0} milliseconds", tickTimes.Count, Elapsed.ElapsedMilliseconds);
Console.WriteLine("Average tick frequency = {0:N2} ms", (double)Elapsed.ElapsedMilliseconds / tickTimes.Count);
// Compute min and max deviation from requested frequency
double minDiff = double.MaxValue;
double maxDiff = double.MinValue;
for (int i = 1; i < tickTimes.Count; ++i)
{
double diff = (tickTimes[i] - tickTimes[i - 1]) - TickFrequency;
minDiff = Math.Min(diff, minDiff);
maxDiff = Math.Max(diff, maxDiff);
}
Console.WriteLine("min diff = {0:N4} ms", minDiff);
Console.WriteLine("max diff = {0:N4} ms", maxDiff);
Console.WriteLine("Test complete. Press Enter.");
Console.ReadLine();
}
}
}
【问题讨论】:
-
好问题!如果有人真的对此有所了解,我很感兴趣。 SO clr 团队的任何人?
-
我敢打赌,win9x 在内核中也有滴答声。毕竟这是一个先发制人的任务环境。都很糟糕,但仍然是真正的多任务。因此,您无法在没有中断的情况下进行抢占,并且它们往往会从常规硬件计时器触发,尤其是在 90 年代,基于 HTC (64 Hz)。没有事件队列如何做 GUI?定时器事件被内核推送到事件队列,没有办法绕过它,因为当你的应用程序在等待队列时什么都不做,它被取消调度了。