【问题标题】:Why are .NET timers limited to 15 ms resolution?为什么 .NET 计时器的分辨率限制为 15 毫秒?
【发布时间】: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

High resolution timer in .NET

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

此外,推荐的 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?定时器事件被内核推送到事件队列,没有办法绕过它,因为当你的应用程序在等待队列时什么都不做,它被取消调度了。

标签: .net timer


【解决方案1】:

也许此处链接的文档对此进行了一些解释。有点干所以我只是快速浏览它:)

引用简介:

系统计时器分辨率决定 Windows 执行两次的频率 主要行动:

  • 更新计时器刻度 如果一个完整的刻度已经过去,则计数。
  • 检查是否有定时定时器对象 已过期。

计时器滴答是经过的概念 Windows 用来跟踪 一天中的时间和线程量子时间。 默认情况下,时钟中断和 计时器滴答声相同,但 Windows 或者应用程序可以更改时钟 中断期。

默认计时器 Windows 7 上的分辨率为 15.6 毫秒 (ms)。一些应用 将其减少到 1 毫秒,从而减少 移动系统上的电池运行时间 高达 25%。

原文来自:Timers, Timer Resolution, and Development of Efficient Code (docx).

【讨论】:

  • 感谢您的链接,阿诺德。这并不能完全回答我的问题,但它确实提供了一些见解,尤其是关于 15.6 毫秒默认计时器分辨率的信息。
  • 是的,我没有感觉到它解释了一切,但有一些有趣的信息。
  • 另请参阅线程"how to set timer resolution from C# to 1 ms?"中的答案。
  • 很好的答案。我一直在做一些测试,试图每 10 毫秒记录一些数据,但结果并不像预期的那样。我做了一些数学运算,发现数据神秘地每 15.63 毫秒记录一次。然后我找到了这个答案,一切都说得通了。谢谢!
  • CLR 定时器内部肯定发生了其他事情。请参阅我的问题示例代码,尽管 OS 计时器分辨率已调整为 1 毫秒,但 .NET 计时器仍保持 15 毫秒分辨率:stackoverflow.com/questions/23215970/…
【解决方案2】:

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

1.通过多媒体定时器界面达到 1 毫秒的分辨率:

多媒体定时器接口能够提供低至 1 毫秒的分辨率。 有关timeBeginPeriod 的更多详细信息,请参阅About Multimedia Timers (MSDN)、Obtaining and Setting Timer Resolution (MSDN) 和this 答案。注意:完成后不要忘记调用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 NtSetTimerResolution() 获得 0.5 毫秒 的分辨率。 NtSetTimerResolution 由本机 Windows NT 库 NTDLL.DLL 导出。请参阅 MSDN 上的 How to set timer resolution to 0.5ms ?。然而,真正可实现的分辨率取决于底层硬件。现代硬件确实支持 0.5 毫秒的分辨率。 在Inside Windows NT High Resolution Timers 中可以找到更多详细信息。支持的分辨率可以通过调用 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 的功能基本上通过使用布尔值Set 映射到函数timeBeginPeriod timeEndPeriod(有关该方案的更多详细信息,请参阅Inside Windows NT High Resolution Timers 和所有其影响)。但是,多媒体套件将粒度限制为毫秒,而 NtSetTimerResolution 允许设置亚毫秒值。

【讨论】:

  • 根据我的实验,我认为 STATUS_TIMER_RESOLUTION_NOT_SET 错误代码应该是 0xC0000245 而不是 245
  • @RolandPihlakas 是的,谢谢。我已编辑我的答案以更正此问题。
  • 所有的例子,以及微软链接文档都是用 C++ 编写的,这显然也可以在 C# 中完成吗? (在我尝试“移植”它之前)
【解决方案3】:

这里的所有回放都是关于系统计时器分辨率的。 但是 .net 计时器尊重它。作为作者自己的通知:

运行时实现自己的定时器队列,类似于 Timer Queue Timers,但从未真正调用 Win32 函数。

Jan 指出comment

因此,上面的答案是很好的信息,但与 .net 计时器没有直接关系,因此会误导人们:(

两个作者问题的简短回答是设计。为什么他们决定走这条路?担心整个系统的性能?谁知道...
为了不重复,请参阅有关这两个问题的更多信息(以及实施精确的方法 .net 上的计时器)在 Jan 的相关 topic 中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-06-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-12
    • 1970-01-01
    • 2011-11-06
    相关资源
    最近更新 更多