System.Threading.Timer vs System.Threading.Thread.Sleep resolution - .NET Timer不使用系统时钟分辨率

Jan*_*Jan 9 .net c# multithreading scheduling timer

问题:System.Threading.Timer尽管OS时钟分辨率更精确, 为什么保持15ms的分辨率?在没有繁忙的CPU等待的情况下,实现1ms定时事件分辨率的可行方法是什么?

再次强调:在我的情况下,系统计时器的分辨率为1ms(而不是建议重复的问题).所以这不是系统计时器分辨率的问题.因此,在所谓的重复问题中没有有用的信息.

背景: .NET似乎System.Threading.Timer 没有使用系统时钟分辨率 - 它保持了~15ms的分辨率.尽管OS时钟(例如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
Run Code Online (Sandbox Code Playgroud)

输出我的快速测试:

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

测试代码是:

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);
}
Run Code Online (Sandbox Code Playgroud)

EDIT1:

环境: 测试在构建和在.NET 2.0发布版本,3.0,3.5(不CountDownEvent)和4.5在Windows 8(建设9200),服务器2012(9200建立),服务器2008(6001构建SP1)到处都之间显著差异SleepTimer.

为什么这不重复: 正如我发布的那样 - 操作系统计时器分辨率设置为1毫秒(并且也Sleep没有表现出行为).因此,这不是OS定时器分辨率(中断频率)的错误 - 这是特定的System.Threading.Timer.

EDIT2 :( 添加TimeBeginPeriodTimeEndPeriod调用代码 - 强制更改OS定时器分辨率)

nic*_*owe 5

使用从WaitHandle派生的其中一个同步类,例如AutoResetEvent或ManualResetEvent,在调用WaitOne()方法时设置timeout参数.

通过循环调用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;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后你可以使用:

using (new TimePeriod(1))
{
    ////...
}
Run Code Online (Sandbox Code Playgroud)

缺口


Kir*_*san 0

尽管操作系统时钟分辨率更加精确,为什么 System.Threading.Timer 仍保持 15ms 分辨率?

显然是由于实施。System.Threading.Timer(以及 Task.Delay)使用 .NET 运行时计时器队列,不考虑系统计时器解析。此外,我运行了测试(.net 4.x)窗口(7、10;服务器 2012、2016),发现 WaitHandle.WaitOne() 和 Monitor.Wait() 也不尊重WinForms GUI 线程上的系统计时器分辨率(上面的答案是使用WaitHandle)。因此,只有 Thread.Sleep 在 GUI 线程上尊重它。

在不忙于 CPU 等待的情况下实现 1ms 定时事件分辨率的推荐方法是什么?

吉姆·米歇尔指出的一种方法。但是,它有一些缺点,例如:在Windows线程池线程
上执行回调。 时间间隔是对于当前时间的。 时间间隔为整数毫秒,因此理论上最大精度为1毫秒。 根据许多报告,1.5-2 毫秒的精度实际上是您可以实现的最大值,并且只能通过 timeBeginPeriod(1) 调用实现。


另一种方法是:NtSetTimerResolutionWaitable Timer Objects。您可以获得 0.5 毫秒的分辨率(取决于硬件和 Windows 版本)。
对于c#示例(这不是您的计时器类的示例,而是在c#中使用此函数的示例),您可以查看这篇文章

您也可以尝试Nick 的建议,但需要记住 GUI 线程的问题。