System.Threading.Timer和System.Threading.Thread.Sleep分辨率的比较——.NET Timer不使用系统时钟分辨率

11
问题: 为什么在操作系统时钟分辨率更高的情况下,System.Threading.Timer仍保持15毫秒的分辨率? 如何实现1毫秒时间事件分辨率而不需要繁忙的CPU等待?
再次强调:在我的情况下,系统定时器具有1ms分辨率(与问题所示的重复问题不同)。因此,所谓的重复问题中没有有用的信息。
背景: 似乎.NET System.Threading.Timer没有使用系统时钟分辨率,而是保持约15ms的分辨率。尽管操作系统时钟(例如Sleep分辨率)更加精确。
在我的计算机上(几乎空闲且有4个核心可运行):
>Clockres.exe

ClockRes v2.0 - View the system clock resolution
Copyright (C) 2009 Mark Russinovich
SysInternals - www.sysinternals.com

Maximum timer interval: 15.625 ms
Minimum timer interval: 0.500 ms
Current timer interval: 1.001 ms

我的快速测试输出结果:

Sleep test:
Average time delta: 2[ms] (from 993 cases)
System.Threading.Timer test:
Average time delta: 15[ms] (from 985 cases)

测试代码所在位置:

private static void TestSleepVsTimer(long millisecondsDifference, int repetions)
{
    TimingEventsKeeper timingEventsKeeper = new TimingEventsKeeper();
    timingEventsKeeper.Reset((int) millisecondsDifference, repetions);

    while (!timingEventsKeeper.TestDoneEvent.IsSet)
    {
        timingEventsKeeper.CountNextEvent(null);
        Thread.Sleep((int) millisecondsDifference);
    }

    Console.WriteLine("Sleep test: ");
    timingEventsKeeper.Output();

    timingEventsKeeper.Reset((int) millisecondsDifference, repetions);

    Timer t = new Timer(timingEventsKeeper.CountNextEvent, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(1));
    timingEventsKeeper.TestDoneEvent.Wait();

    Console.WriteLine("System.Threading.Timer test: ");
    timingEventsKeeper.Output();
}

private class TimingEventsKeeper
{
    long _ticksSum = 0;
    long _casesCount = 0;
    long _minTicksDiff;
    long _maxTicksDiff;
    long _lastTicksCount;
    int _repetitons;

    public CountdownEvent TestDoneEvent = new CountdownEvent(0);

    public void Reset(int millisecondsDifference, int repetitions)
    {
        _ticksSum = 0;
        _casesCount = 0;
        _minTicksDiff = millisecondsDifference * 10000;
        _maxTicksDiff = millisecondsDifference * 10000;
        _lastTicksCount = DateTime.UtcNow.Ticks;
        _repetitons = repetitions;
        TestDoneEvent.Reset(repetitions);
    }

    public void CountNextEvent(object unused)
    {
        long currTicksCount = DateTime.UtcNow.Ticks;
        long diff = currTicksCount - _lastTicksCount;
        _lastTicksCount = currTicksCount;

        TestDoneEvent.Signal();

        if (diff >= _maxTicksDiff)
        {
            _maxTicksDiff = diff;
            return;
        }

        if (diff <= _minTicksDiff)
        {
            _minTicksDiff = diff;
            return;
        }

        _casesCount++;
        _ticksSum += diff;

    }

    public void Output()
    {
        if(_casesCount > 0)
            Console.WriteLine("Average time delta: {0}[ms] (from {1} cases)", _ticksSum / _casesCount / 10000, _casesCount);
        else
            Console.WriteLine("No measured cases to calculate average");
    }
}

public static class WinApi
{
    /// <summary>TimeBeginPeriod(). See the Windows API documentation for details.</summary>

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Interoperability", "CA1401:PInvokesShouldNotBeVisible"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2118:ReviewSuppressUnmanagedCodeSecurityUsage"), SuppressUnmanagedCodeSecurity]
    [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", SetLastError = true)]

    public static extern uint TimeBeginPeriod(uint uMilliseconds);

    /// <summary>TimeEndPeriod(). See the Windows API documentation for details.</summary>

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Interoperability", "CA1401:PInvokesShouldNotBeVisible"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2118:ReviewSuppressUnmanagedCodeSecurityUsage"), SuppressUnmanagedCodeSecurity]
    [DllImport("winmm.dll", EntryPoint = "timeEndPeriod", SetLastError = true)]

    public static extern uint TimeEndPeriod(uint uMilliseconds);
}

private static void Main(string[] args)
{
    WinApi.TimeBeginPeriod(1);
    TestSleepVsTimer(1, 1000);
    WinApi.TimeEndPeriod(1);
}

编辑1:

环境: 在 .NET 2.0、3.0、3.5(不包括 CountDownEvent)和 4.5 版本下的构建和发布版本中进行了测试 在 Windows 8 (Build 9200)、Server 2012 (Build 9200)、Server 2008 (Build 6001 SP1) 上 任何存在 SleepTimer 显著差异的地方。

为什么这不是重复问题: 正如我所发帖子的内容 - 操作系统定时器分辨率被设置为 1ms (并且 Sleep 不会表现出此行为)。因此,这不是操作系统定时器分辨率(中断频率)的故障 - 这是特定于 System.Threading.Timer 的问题。

编辑2: (添加了对代码的 TimeBeginPeriodTimeEndPeriod 调用 - 强制更改操作系统定时器分辨率)


4
根据我的建议,如果需要达到1毫秒的精度,最好不要使用Windows操作系统。因为Windows不是一个实时操作系统 - Lasse V. Karlsen
2
这曾经是有效的。然而,System.Threading.Timer的底层实现在不同的.NET版本中已经多次更改。您使用的Windows版本也很重要,您没有记录版本号。应用程序可以使用timeBeginPeriod()任意更改时钟滴答间隔,Chrome是这样做的最好例子。这会影响每个程序中计时器的行为,这并不是一个功能,可能已经在以后的.NET版本中得到解决。真正的代码在CLR中很难看到。 - Hans Passant
感谢您的评论,我添加了更多澄清编辑。 基本上 - 这似乎不是操作系统时间分辨率的问题,而是Threading.Timer中的某些问题。 - Jan
1
为什么 System.Threading.Timer 限制在15毫秒内还是个谜。但事实就是这样。我放弃了试图搞清楚原因,只是编写了自己的接口来使用Windows Timer Queue计时器。请参阅使用Windows Timer Queue API - Jim Mischel
1
Jim的链接已经失效了,所以这里提供一个来自WebArchive的链接:https://web.archive.org/web/20160831052816/http://www.informit.com/guides/content.aspx?g=dotnet&seqNum=817 - nb1forxp
显示剩余2条评论
2个回答

5

使用从WaitHandle派生的同步类之一,如AutoResetEvent或ManualResetEvent,在调用WaitOne()方法时设置超时参数。

通过在循环中调用WaitOne,您可以实现一个计时器。

您可以发出信号以打破或中断计时器的等待句柄派生类。

注意,要更改分辨率,最好使用实现IDisposable的辅助类:

internal sealed class TimePeriod : IDisposable
{
    private const string WINMM = "winmm.dll";

    private static TIMECAPS timeCapabilities;

    private static int inTimePeriod;

    private readonly int period;

    private int disposed;

    [DllImport(WINMM, ExactSpelling = true)]
    private static extern int timeGetDevCaps(ref TIMECAPS ptc, int cbtc);

    [DllImport(WINMM, ExactSpelling = true)]
    private static extern int timeBeginPeriod(int uPeriod);

    [DllImport(WINMM, ExactSpelling = true)]
    private static extern int timeEndPeriod(int uPeriod);

    static TimePeriod()
    {
        int result = timeGetDevCaps(ref timeCapabilities, Marshal.SizeOf(typeof(TIMECAPS)));
        if (result != 0)
        {
            throw new InvalidOperationException("The request to get time capabilities was not completed because an unexpected error with code " + result + " occured.");
        }
    }

    internal TimePeriod(int period)
    {
        if (Interlocked.Increment(ref inTimePeriod) != 1)
        {
            Interlocked.Decrement(ref inTimePeriod);
            throw new NotSupportedException("The process is already within a time period. Nested time periods are not supported.");
        }

        if (period < timeCapabilities.wPeriodMin || period > timeCapabilities.wPeriodMax)
        {
            throw new ArgumentOutOfRangeException("period", "The request to begin a time period was not completed because the resolution specified is out of range.");
        }

        int result = timeBeginPeriod(period);
        if (result != 0)
        {
            throw new InvalidOperationException("The request to begin a time period was not completed because an unexpected error with code " + result + " occured.");
        }

        this.period = period;
    }

    internal static int MinimumPeriod
    {
        get
        {
            return timeCapabilities.wPeriodMin;
        }
    }

    internal static int MaximumPeriod
    {
        get
        {
            return timeCapabilities.wPeriodMax;
        }
    }

    internal int Period
    {
        get
        {
            if (this.disposed > 0)
            {
                throw new ObjectDisposedException("The time period instance has been disposed.");
            }

            return this.period;
        }
    }

    public void Dispose()
    {
        if (Interlocked.Increment(ref this.disposed) == 1)
        {
            timeEndPeriod(this.period);
            Interlocked.Decrement(ref inTimePeriod);
        }
        else
        {
            Interlocked.Decrement(ref this.disposed);
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct TIMECAPS
    {
        internal int wPeriodMin;

        internal int wPeriodMax;
    }
}

你可以使用以下代码:
using (new TimePeriod(1))
{
    ////...
}

Nick


0
System.Threading.Timer 为什么保持15ms的分辨率,尽管操作系统时钟分辨率更精确?
显然是由于实现的原因。 System.Threading.Timer(因此也适用于Task.Delay)使用.NET运行时定时器队列,该队列不考虑系统定时器分辨率。此外,我在Windows(7、10;Server 2012、2016),.net 4.x上进行了测试,并发现WaitHandle.WaitOne()和Monitor.Wait()在WinForms GUI线程上也不考虑系统计时器分辨率(这对于以上回答中的{{link1:answer}}的使用来说很重要)。因此,在GUI线程上只有Thread.Sleep会被考虑。
如何在没有繁忙CPU等待的情况下实现1ms的时间事件分辨率?

一种方法是由Jim Mischel指出的。但是,它有以下缺点:
回调在windows线程池线程上执行。
时间间隔与当前时间相关
时间间隔是整数毫秒,理论上最大精度为1毫秒。
根据许多报告,实际上只能达到1.5-2毫秒的精度,而且只有通过timeBeginPeriod(1)调用才能达到。

另一种方法是:NtSetTimerResolutionWaitable Timer Objects。您可以获得0.5毫秒的分辨率(取决于硬件和Windows版本)。
对于C#示例(它不是您计时器类的示例,而是使用此函数在C#中的示例),您可以查看此文章
您还可以尝试Nick的建议,但需要记住GUI线程的问题。

我尝试了Jim Mischel的方法,使用timeBeginPeriod(1),我的分辨率在Windows 10上仍然保持在15毫秒。 - Allstar

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