为什么.NET定时器被限制在15 ms的分辨率?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (70)

请注意,我正在询问一些使用类似的东西,每15毫秒会更频繁地调用一次回调函数System.Threading.Timer。我不是在问如何使用类似System.Diagnostics.Stopwatch或甚至是准确的时间来编写一段代码QueryPerformanceCounter

另外,推荐的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事实证明,它不是定时器队列定时器的包装。至少不在2.0运行时,它是从.NET 2.0到.NET 3.5使用的。几分钟的Shared Source CLI显示运行时实现了自己的定时器队列,它与定时器队列定时器类似,但从未实际调用Win32函数。

看起来.NET 4.0运行时也实现了它自己的计时器队列。我的测试程序(见下文)在.NET 4.0下提供了类似的结果,就像它在.NET 3.5下一样。我为定时器队列定时器创建了自己的托管包装,并证明我可以获得1毫秒的分辨率(具有相当高的准确性),所以我认为我不太可能读错了CLI源。

我有两个问题:

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

其次,我意识到这很难回答,为什么不使用定时器队列定时器?我意识到.NET 1.x必须在Win9x上运行,它没有这些API,但是它们自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();
        }
    }
}
提问于
用户回答回答于

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

引用介绍:

系统计时器分辨率决定了Windows执行两个主要操作的频率:

  • 如果完整的时间间隔已过,请更新计时器滴答计数。
  • 检查计划的计时器对象是否已过期。

计时器滴答是Windows用来追踪一天中的时间和线程量子时间的经过时间的概念。默认情况下,时钟中断和计时器滴答是相同的,但Windows或应用程序可以更改时钟中断周期。 Windows 7上的默认计时器分辨率为15.6毫秒(ms)。一些应用程序将其减少到1 ms,这将移动系统上的电池运行时间缩短了25%。

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

用户回答回答于

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

1.通过多媒体计时器接口以1毫秒的分辨率进行:

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

怎么做:

#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 获得0.5毫秒的分辨率NtSetTimerResolution()。NtSetTimerResolution由本机Windows NT库NTDLL.DLL导出。请参阅如何将定时器分辨率设置为0.5ms?在MSDN上。不过,真正可实现的解决方案是由底层硬件决定的。现代硬件支持0.5毫秒的分辨率。在Windows NT高分辨率定时器中可以找到更多详细信息。支持的分辨率可以通过调用NtQueryTimerResolution()来获得。

怎么做:

#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\n",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]\n",CurrentResolution);
        break;
    case STATUS_TIMER_RESOLUTION_NOT_SET:
        printf("The requested resolution was not set\n");   
        // 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的功能基本上映射到函数timeBeginPeriod timeEndPeriod使用bool值Set(有关该方案及其所有含义的更多详细信息,请参阅Windows NT高分辨率定时器内部)。但是,多媒体套件将粒度限制为毫秒,NtSetTimerResolution允许设置亚毫秒值。

扫码关注云+社区