High resolution timer in C#
Asked Answered
S

5

21

Is there a high resolution timer that raises an event each time the timer elapses, just like the System.Timer class? I need a high resolution timer to Elapse every ms.

I keep running into posts that explain that the Stopwatch can measure high resolutions, but I don't want to measure time, I want to create an interval of 1 ms.

Is there something in .NET or am I going to write my own high res timer?

Sisto answered 19/7, 2014 at 10:2 Comment(9)
When you use winforms, you do have a timer in toolbox. And you can set its interval to 1 milliseconds. What else do you need?Lamonicalamont
What exactly are you trying to do once a millisecond? A millisecond isn't exactly a lot of time.Chickabiddy
@MatinLotfaliee I need it to actually act on the interval of 1ms, which it doesn't.Sisto
@nvoigt, simulating the real bus system in our machine, which has a tact time of 1msSisto
What do you mean by "tact time"? A bus won't actually be stopping at different stops every millisecond...Maryjomaryl
Why do you say it doesn't? I believe it does... I don't understand.Lamonicalamont
Also, https://mcmap.net/q/382237/-thread-sleep-for-less-than-1-millisecondBever
@JohnSkeet, Tact time like in every ms we receive an update from our EtherCat bus, where we can read the current sensor values and set new actuators. So what I am trying to do, is simulate that bus (which goes fine with a Timer up and until 15ms). I use the Thread example below now (which seems to work for 1ms as well). I know this is stretching the limits on a not real time OS. On the other hand, the world doesn't burn down to ashes when we miss a few tacts now and then. So the question I have left for you is: is there a better alternative then relying on Thread.Sleep(1)?Sisto
In one of the answers in the linked post there was a hint at SpinWait in conjunction with checking the current time. This sounded promising, imo.Inviolable
W
28

There is nothing built into the .NET framework that I am aware of. Windows has a mechanism for high resolution timer events via the Multimedia Timer API. Below is a quick example I whipped up which seems to do the job. There are also seems to be a good example here.

I will note that this API changes system wide settings that can degrade system performance, so buyer beware. For testing purposes, I would recommend keeping track of how often the timer is firing to verify the timing is similar to the device you are trying to simulate. Since windows is not a real-time OS, the load on your system may cause the MM timer be delayed resulting in gaps of 100 ms that contain 100 events in quick succession, rather than 100 events spaced 1 ms apart. Some additional reading on MM timers.

class Program
{
    static void Main(string[] args)
    {
        TestThreadingTimer();
        TestMultimediaTimer();
    }

    private static void TestMultimediaTimer()
    {
        Stopwatch s = new Stopwatch();
        using (var timer = new MultimediaTimer() { Interval = 1 })
        {
            timer.Elapsed += (o, e) => Console.WriteLine(s.ElapsedMilliseconds);
            s.Start();
            timer.Start();
            Console.ReadKey();
            timer.Stop();
        }
    }

    private static void TestThreadingTimer()
    {
        Stopwatch s = new Stopwatch();
        using (var timer = new Timer(o => Console.WriteLine(s.ElapsedMilliseconds), null, 0, 1))
        {
            s.Start();
            Console.ReadKey();
        }
    }

}

public class MultimediaTimer : IDisposable
{
    private bool disposed = false;
    private int interval, resolution;
    private UInt32 timerId; 

    // Hold the timer callback to prevent garbage collection.
    private readonly MultimediaTimerCallback Callback;

    public MultimediaTimer()
    {
        Callback = new MultimediaTimerCallback(TimerCallbackMethod);
        Resolution = 5;
        Interval = 10;
    }

    ~MultimediaTimer()
    {
        Dispose(false);
    }

    public int Interval
    {
        get
        {
            return interval;
        }
        set
        {
            CheckDisposed();

            if (value < 0)
                throw new ArgumentOutOfRangeException("value");

            interval = value;
            if (Resolution > Interval)
                Resolution = value;
        }
    }

    // Note minimum resolution is 0, meaning highest possible resolution.
    public int Resolution
    {
        get
        {
            return resolution;
        }
        set
        {
            CheckDisposed();

            if (value < 0)
                throw new ArgumentOutOfRangeException("value");

            resolution = value;
        }
    }

    public bool IsRunning
    {
        get { return timerId != 0; }
    }

    public void Start()
    {
        CheckDisposed();

        if (IsRunning)
            throw new InvalidOperationException("Timer is already running");

        // Event type = 0, one off event
        // Event type = 1, periodic event
        UInt32 userCtx = 0;
        timerId = NativeMethods.TimeSetEvent((uint)Interval, (uint)Resolution, Callback, ref userCtx, 1);
        if (timerId == 0)
        {
            int error = Marshal.GetLastWin32Error();
            throw new Win32Exception(error);
        }
    }

    public void Stop()
    {
        CheckDisposed();

        if (!IsRunning)
            throw new InvalidOperationException("Timer has not been started");

        StopInternal();
    }

    private void StopInternal()
    {
        NativeMethods.TimeKillEvent(timerId);
        timerId = 0;
    }

    public event EventHandler Elapsed;

    public void Dispose()
    {
        Dispose(true);
    }

    private void TimerCallbackMethod(uint id, uint msg, ref uint userCtx, uint rsv1, uint rsv2)
    {
        var handler = Elapsed;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }

    private void CheckDisposed()
    {
        if (disposed)
            throw new ObjectDisposedException("MultimediaTimer");
    }

    private void Dispose(bool disposing)
    {
        if (disposed)
            return;
        
        disposed = true;
        if (IsRunning)
        {
            StopInternal();
        }
        
        if (disposing)
        {
            Elapsed = null;
            GC.SuppressFinalize(this);
        }
    }
}

internal delegate void MultimediaTimerCallback(UInt32 id, UInt32 msg, ref UInt32 userCtx, UInt32 rsv1, UInt32 rsv2);

internal static class NativeMethods
{
    [DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeSetEvent")]
    internal static extern UInt32 TimeSetEvent(UInt32 msDelay, UInt32 msResolution, MultimediaTimerCallback callback, ref UInt32 userCtx, UInt32 eventType);

    [DllImport("winmm.dll", SetLastError = true, EntryPoint = "timeKillEvent")]
    internal static extern void TimeKillEvent(UInt32 uTimerId);
}
Wellstacked answered 19/7, 2014 at 19:5 Comment(4)
This worked out of the box. I am using it in production code with attribution.Only problem is it crashes unless you create the callback delegate yourself and use GCHandle.Alloc() to prevent it from moved or deleted...Wulfe
I think I'm running into this exact issue -- A callback was made on a garbage collected delegate of type ... MultimediaTimerCallback::Invoke. Could you provide more insight on how you solved this?Urbain
I believe I've fixed crashes related to the timer callback being GC'd.Wellstacked
I've posted the code on GitHub, so any future issues can be raised over there.Wellstacked
G
2

I couldn't get Mike's solution to work and created a basic wrapper around Windows multi media timer based on this codeproject article https://www.codeproject.com/Articles/17474/Timer-surprises-and-how-to-avoid-them

public class WinMMWrapper
{
    [DllImport("WinMM.dll", SetLastError = true)]
    public static extern uint timeSetEvent(int msDelay, int msResolution,
        TimerEventHandler handler, ref int userCtx, int eventType);

    public delegate void TimerEventHandler(uint id, uint msg, ref int userCtx,
        int rsv1, int rsv2);

    public enum TimerEventType
    {
        OneTime = 0,
        Repeating = 1
    }

    private readonly Action _elapsedAction;
    private readonly int _elapsedMs;
    private readonly int _resolutionMs;
    private readonly TimerEventType _timerEventType;
    private readonly TimerEventHandler _timerEventHandler;

    public WinMMWrapper(int elapsedMs, int resolutionMs, TimerEventType timerEventType, Action elapsedAction)
    {
        _elapsedMs = elapsedMs;
        _resolutionMs = resolutionMs;
        _timerEventType = timerEventType;
        _elapsedAction = elapsedAction;
        _timerEventHandler = TickHandler;
    }

    public uint StartElapsedTimer()
    {
        var myData = 1; //dummy data
        return timeSetEvent(_elapsedMs, _resolutionMs / 10, _timerEventHandler, ref myData, (int)_timerEventType);
    }

    private void TickHandler(uint id, uint msg, ref int userctx, int rsv1, int rsv2)
    {
        _elapsedAction();
    }
}

Here's an example how to use it

class Program
{
    static void Main(string[] args)
    {
        var timer = new WinMMWrapper(100, 25, WinMMWrapper.TimerEventType.Repeating, () =>
        {
            Console.WriteLine($"Timer elapsed {DateTime.UtcNow:o}");
        });

        timer.StartElapsedTimer();

        Console.ReadKey();
    }
}

The output looks like this

enter image description here

Update 2021-11-19: add TimerEventHandler class member per chris's comment.

Geomorphic answered 12/4, 2021 at 14:19 Comment(2)
Great answer, and this works pretty well. One thing I found was that the TimerEventHandler is getting garbage collected in the WinMMWrapper. This can be fixed by making the handler a class level variable, then passing it into timeSetEvent. I guess this is a common unmanaged code issue.Beliabelial
Hi, the timer works very well as resolution. But it disappears with the garbage collector. How can I prevent it?Venterea
C
1

There is an option: use Thread.Sleep(0). Attempt to call Thread.Sleep(1) or employ a System.Threading.Timer would always come down to system timer resolution. Depending on one is probably not the best idea, at the end of the day you app might be just not allowed to call timeBeginPeriod(...) from winmm.dll.

Following code can resolve down to +/- 10ns (0.10ms) on my dev machine (i7q) and could be higher. It would put a solid load on one of your CPU cores pushing its use up to 100%. No actual OS slowdown would happen, the code surrenders most of its CPU time quantum by calling Thread.Sleep as early as possible:

var requiredDelayMs = 0.1;
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
while (true)
{
    if (sw.Elapsed.TotalMilliseconds >= requiredDelayMs) 
    {
      // call your timer routine
    }
    Thread.Sleep(0); // setting at least 1 here would involve a timer which we don't want to
}

For the more comprehensive implementation see my other answer

Chazan answered 3/4, 2019 at 20:16 Comment(2)
Thread.Yield() will reduce the chance of a context-switch. Thread.Sleep(0) results in a context switch to lower priority processes. Elevating the current process priority will decrease the chance of Thread.Yield() itself context-switching.Gasper
Additionally, SpinWait can be used to package everything together, even reducing the chance of a Yield context switch: "It is carefully implemented to provide correct spinning behavior for the general case, and will itself initiate context switches if it spins long enough (roughly the length of time required for a kernel transition)."Gasper
X
0

Precision-Timer.NET

https://github.com/HypsyNZ/Precision-Timer.NET https://www.nuget.org/packages/PrecisionTimer.NET/

A High Precision .NET timer that doesn't kill your CPU or get Garbage Collected.

Its designed to be as easy to use as any other .NET timer.

Xena answered 30/1, 2022 at 0:21 Comment(0)
A
-4

Try creating new System.Threading.Thread and using System.Threading.Thread.Sleep.

var thrd = new Syatem.Threading.Thread(() => {
    while (true) {
        // do something
        System.Threading.Thread.Sleep(1); // wait 1 ms
    }
});

thrd.Start();
Asti answered 19/7, 2014 at 10:29 Comment(4)
That doesn't guarantee thread will wakeup sharply in 1 milli secondsErme
The sleep interval will actually be about 12-15ms. That is the minimum resolution provided by the system clock.Wellstacked
note: I have two windows 10 laptops (i5 and i7) the i7 has 1ms accuracy and the i5 (MS Surface) has 15.6 ms accuracy using thread.sleep(). So it might work on your dev machine, it might not in prod.Precontract
@markgamache this has changed in Windows 10 at some point; timeBeginPeriod behavior has been changed so timing can vary depending on the Win10 version.Gravettian

© 2022 - 2024 — McMap. All rights reserved.