System.Threading.Timer vs System.Threading.Thread.Sleep resolution - .NET Timer not using system clock resolution

StackOverflow https://stackoverflow.com/questions/23215970

Question

Questions: Why is the System.Threading.Timer keeping the 15ms resolution despite the OS clock resolution is much more precise? What is the recommendable way to achieve 1ms timing events resolution without busy CPU waiting?

To stress once more: System timer has 1ms resolution in my case (as opposed to the question suggested as duplicate). So this is not an issue of system timer resolution. Therefore, there is no useful info in the supposedly duplicate question.

Background: It seems that .NET System.Threading.Timer is not using system clock resolution - it keeps the ~ 15ms resolution. Despite the OS clock (and e.g. Sleep resolution) is much more precise.

On my box (when almost idle and 4 cores are available to run):

>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

Output of my quick test:

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

Where the test code is:

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);
}

EDIT1:

Environment: Tested on Build and Release version under .NET 2.0, 3.0, 3.5 (without CountDownEvent) and 4.5 On Windows 8 (Build 9200), Server 2012 (Build 9200), Server 2008 (Build 6001 SP1) Everywhere with significant difference between Sleep and Timer.

Why this is not duplicate: As I posted - the OS timer resolution is set to 1ms (and also Sleep doesn't exhibit the behavior). Therefore this is not fault of OS timer resolution (interrupts frequency) - this is something specific to System.Threading.Timer.

EDIT2: (Added TimeBeginPeriod and TimeEndPeriod calls to code - to force OS timer resolution change)

Was it helpful?

Solution 2

Why is the System.Threading.Timer keeping the 15ms resolution despite the OS clock resolution is much more precise?

Obviously due to implementation. System.Threading.Timer (and therefore Task.Delay) uses .NET runtime timer queue, that not respects system timer resolution. Furthermore, I ran tests (.net 4.x) windows (7, 10; server 2012, 2016) and found, that WaitHandle.WaitOne() and Monitor.Wait() doesn't respect system timer resolution on WinForms GUI thread too (it's for answer above to use WaitHandle). So, only Thread.Sleep respect it on GUI thread.

What is the recommendable way to achieve 1ms timing events resolution without busy CPU waiting?

One way pointed out by Jim Mischel. But, it have drawbacks like:
Callback executing on windows thread pool thread.
Time interval is relative to the current time.
Time interval is integer ms, so theoretically max precision is 1 ms.
By many reports, 1.5-2 ms precision is practically maximum that you can achieve and only with timeBeginPeriod(1) call.

Another approach is: NtSetTimerResolution and Waitable Timer Objects. You may obtain 0.5 ms resolution (depends on hardware and windows version).
For c# example (It's not the example of your timer class, but example of using this functions in c#), you can check this article.

You also can try Nick's suggestion but need to keep in mind problems with GUI thread.

OTHER TIPS

Use one of the synchronisation classes that derives from WaitHandle, such as AutoResetEvent or ManualResetEvent, setting the timeout parameter when you call the WaitOne() method.

By calling WaitOne in a loop, you can implement a timer.

You can signal the wait handle derived class to break out of or interrupt the timer.

Note, to change the resolution, you are best off using a helper class that implements 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;
    }
}

You can then use:

using (new TimePeriod(1))
{
    ////...
}

Nick

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top