为什么.NET计时器的分辨率限制在15毫秒?

Why are .NET timers limited to 15 ms resolution?

请注意,我问的是使用类似System.Threading.Timer之类的东西,每隔15毫秒就会调用一次回调函数。我不是在询问如何使用System.Diagnostics.Stopwatch甚至QueryPerformanceCounter之类的内容准确计算代码。

另外,我已经阅读了相关问题:

准确的Windows计时器? System.Timers.Timer()限制为15毫秒

.NET中的高分辨率计时器

这些都没有为我的问题提供有用的答案。

此外,推荐的MSDN文章"为Windows实现连续更新,高分辨率时间提供程序"是关于计时而不是提供连续的滴答流。

照这样说。 。 。

关于.NET计时器对象有很多不好的信息。例如,System.Timers.Timer被称为"针对服务器应用程序优化的高性能计时器"。并且System.Threading.Timer被某种方式视为二等公民。传统观点认为System.Threading.Timer是Windows计时器队列计时器的包装器,System.Timers.Timer完全是另一回事。

现实情况大不相同。 System.Timers.Timer只是System.Threading.Timer的一个瘦组件包装器(只需使用Reflector或ILDASM来查看System.Timers.Timer内部,你会看到对System.Threading.Timer的引用),并且有一些代码可以提供自动线程同步,所以你不必这样做。

System.Threading.Timer,因为事实证明它不是Timer Queue Timers的包装器。至少不在2.0运行时,它是从.NET 2.0到.NET 3.5使用的。使用Shared Source CLI几分钟后,运行时会实现自己的定时器队列,类似于Timer Queue Timers,但实际上从不调用Win32函数。

似乎.NET 4.0运行时还实现了自己的计时器队列。我的测试程序(见下文)在.NET 4.0下提供与.NET 3.5相似的结果。我已经为Timer Queue Timers创建了自己的托管包装器并证明我可以获得1 ms的分辨率(具有相当好的准确性),所以我认为我不太可能错误地读取CLI源代码。

我有两个问题:

首先,是什么原因导致运行时计时器队列的实现如此缓慢?我的分辨率不能超过15毫秒,精度似乎在-1到+30毫秒的范围内。也就是说,如果我要求24毫秒,我会在23到54毫秒之间的任何地方得到滴答声。我想我可以花更多时间使用CLI源来追踪答案,但我想这里有人可能知道。

其次,我意识到这很难回答,为什么不使用定时器队列定时器?我意识到.NET 1.x必须在没有这些API的Win9x上运行,但它们自Windows 2000以来就存在,如果我记得正确的话,它是.NET 2.0的最低要求。是因为CLI必须在非Windows机器上运行吗?

我的计时器测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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();
        }
    }
}


也许这里链接的文件解释了一下。它有点干,所以我很快就浏览了:)

引用介绍:

The system timer resolution determines
how frequently Windows performs two
main actions:

  • Update the timer tick
    count if a full tick has elapsed.
  • Check whether a scheduled timer object
    has expired.

A timer tick is a notion of elapsed
time that Windows uses to track the
time of day and thread quantum times.
By default, the clock interrupt and
timer tick are the same, but Windows
or an application can change the clock
interrupt period.

The default timer
resolution on Windows 7 is 15.6
milliseconds (ms). Some applications
reduce this to 1 ms, which reduces the
battery run time on mobile systems by
as much as 25 percent.

最初来自:定时器,定时器分辨率和高效代码开发(docx)。


计时器分辨率由系统心跳给出。这通常默认为64次/秒,即15.625毫秒。但是,有些方法可以修改这些系统范围的设置,以便在较新的平台上实现低至1毫秒甚至0.5毫秒的定时器分辨率:

1.通过多媒体计时器界面获得1 ms分辨率:

多媒体定时器接口能够提供低至1毫秒的分辨率。
有关timeBeginPeriod的更多详细信息,请参阅关于多媒体计时器(MSDN),获取和设置计时器分辨率(MSDN)以及此答案。注意:完成后不要忘记调用timeEndPeriod切换回默认的计时器分辨率。

怎么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define TARGET_RESOLUTION 1         // 1-millisecond target resolution

TIMECAPS tc;
UINT     wTimerRes;

if (timeGetDevCaps(&tc, sizeof(TIMECAPS)) != TIMERR_NOERROR)
{
   // Error; application can't continue.
}

wTimerRes = min(max(tc.wPeriodMin, TARGET_RESOLUTION), tc.wPeriodMax);
timeBeginPeriod(wTimerRes);

//       do your stuff here at approx. 1 ms timer resolution

timeEndPeriod(wTimerRes);

注意:此过程也适用于其他过程,并且所获得的分辨率适用于整个系统。任何流程要求的最高解决方案都将是活跃的,请注意后果。

2.分辨率为0.5毫秒:

您可以通过隐藏的API NtSetTimerResolution()获得0.5 ms的分辨率。
NtSetTimerResolution由本机Windows NT库NTDLL.DLL导出。请参阅如何将计时器分辨率设置为0.5毫秒?在MSDN上。然而,真正可实现的解决方案由底层硬件决定。现代硬件支持0.5毫秒的分辨率。
更多详细信息可在Inside Windows NT高分辨率计时器中找到。支持的分辨率可以通过调用NtQueryTimerResolution()获得。

怎么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#define STATUS_SUCCESS 0
#define STATUS_TIMER_RESOLUTION_NOT_SET 0xC0000245

// after loading NtSetTimerResolution from ntdll.dll:

// The requested resolution in 100 ns units:
ULONG DesiredResolution = 5000;  
// Note: The supported resolutions can be obtained by a call to NtQueryTimerResolution()

ULONG CurrentResolution = 0;

// 1. Requesting a higher resolution
// Note: This call is similar to timeBeginPeriod.
// However, it to to specify the resolution in 100 ns units.
if (NtSetTimerResolution(DesiredResolution ,TRUE,&CurrentResolution) != STATUS_SUCCESS) {
    // The call has failed
}

printf("CurrentResolution [100 ns units]: %d
",CurrentResolution);
// this will show 5000 on more modern platforms (0.5ms!)

//       do your stuff here at 0.5 ms timer resolution

// 2. Releasing the requested resolution
// Note: This call is similar to timeEndPeriod
switch (NtSetTimerResolution(DesiredResolution ,FALSE,&CurrentResolution) {
    case STATUS_SUCCESS:
        printf("The current resolution has returned to %d [100 ns units]
",CurrentResolution);
        break;
    case STATUS_TIMER_RESOLUTION_NOT_SET:
        printf("The requested resolution was not set
");  
        // the resolution can only return to a previous value by means of FALSE
        // when the current resolution was set by this application      
        break;
    default:
        // The call has failed

}

注意:NtSetTImerResolution的功能基本上通过使用bool值Set映射到函数timeBeginPeriodtimeEndPeriod(有关该方案及其所有含义的更多详细信息,请参阅内部Windows NT高分辨率计时器)。但是,多媒体套件将粒度限制为毫秒,NtSetTimerResolution允许设置亚毫秒值。


这里的所有重放都与系统定时器分辨率有关。但.net计时器不尊重它。作者自己注意到:

that the runtime implements its own timer queue that is similar to
the Timer Queue Timers, but never actually calls the Win32 functions.

Jan指出评论。

所以,上面的答案是很好的信息,但不直接与.net计时器相关,因此误导人:(

对两个作者问题的简短回答都是设计上的。他们为什么决定这样走?担心整个系统的性能?谁知道...
要不重复,请查看有关这两个问题的更多信息(以及实现精确问题的方法)
Jan的相关主题中的.net上的计时器。