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

2

11

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)

Boston answered 22/4, 2014 at 9:46 Comment(7)
The recommended way (in my opinion) to get 1ms precision is to not use Windows. Windows is not a real time operating system.Chiarra
This used to work. The underlying implementation of System.Threading.Timer however has been changed several times across different .NET versions. The version of Windows you use matter as well, you didn't document version numbers. Apps can arbitrary change the clock tick interval with timeBeginPeriod(), Chrome is the best example of an app doing this. That this affects the way timers behave in every program is not exactly a feature and may well have been addressed in later .NET versions. Hard to see, the real code is in the CLR.Croom
Thanks for comments I added more clarifying edit. Basically - this doesn't seem to be fault of OS time resolution, rather something in Threading.TimerBoston
@Sriram Sakthivel - can you please kindly review my question as I added reasoning why it is not a duplicate (in short - System.Threading.Timer is not clearly using current OS timer resolution). So the linked question doesn't provide any helpful answer. Many thanksBoston
"Why" System.Threading.Timer limits you to 15 ms is a bit of a mystery. But that's the way it is. I gave up on trying to figure out why and just wrote my own interface to the Windows Timer Queue timers. See Using the Windows Timer Queue API.Qktp
Jim, thanks for sharing the code with queue timers. It has a bug though, I think. The timer callback of type Win32WaitOrTimerCallback is passed to the unmanaged code, but it actually creates a reference like new delegate and it is nowhere stored, and thus gets wiped at first GC run. Then when the timer ticks, exception happens. I added a field in the timer class and stored the reference, it fixed the problem. Letting you know here, because the blog post doesn't have comments.Ratliff
Jim's link is broken, so here's a link from the webarchive: web.archive.org/web/20160831052816/http://www.informit.com/…Imperturbation
E
0

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.

Evaginate answered 26/9, 2018 at 15:59 Comment(1)
I tried Jim Mischel's way, with timeBeginPeriod(1) , my resolution remains at 15ms on Windows 10Screening
M
5

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

Munshi answered 13/6, 2014 at 7:44 Comment(0)
E
0

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.

Evaginate answered 26/9, 2018 at 15:59 Comment(1)
I tried Jim Mischel's way, with timeBeginPeriod(1) , my resolution remains at 15ms on Windows 10Screening

© 2022 - 2024 — McMap. All rights reserved.