Skip to content

Instantly share code, notes, and snippets.

@DraTeots
Last active February 22, 2024 11:38
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save DraTeots/436019368d32007284f8a12f1ba0f545 to your computer and use it in GitHub Desktop.
Save DraTeots/436019368d32007284f8a12f1ba0f545 to your computer and use it in GitHub Desktop.
HighResolutionTimer for .NET
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
namespace HighResolutionTimer
{
/// <summary>
/// Hight precision non overlapping timer
/// Came from
/// https://stackoverflow.com/a/41697139/548894
/// </summary>
/// <remarks>
/// This implementation guaranteed that Elapsed events
/// are not overlapped with different threads.
/// Which is important, because a state of the event handler attached to Elapsed,
/// may be left unprotected of multi threaded access
/// </remarks>
public class HighResolutionTimer
{
/// <summary>
/// Tick time length in [ms]
/// </summary>
public static readonly double TickLength = 1000f / Stopwatch.Frequency;
/// <summary>
/// Tick frequency
/// </summary>
public static readonly double Frequency = Stopwatch.Frequency;
/// <summary>
/// True if the system/operating system supports HighResolution timer
/// </summary>
public static bool IsHighResolution = Stopwatch.IsHighResolution;
/// <summary>
/// Invoked when the timer is elapsed
/// </summary>
public event EventHandler<HighResolutionTimerElapsedEventArgs> Elapsed;
/// <summary>
/// The interval of timer ticks [ms]
/// </summary>
private volatile float _interval;
/// <summary>
/// The timer is running
/// </summary>
private volatile bool _isRunning;
/// <summary>
/// Execution thread
/// </summary>
private Thread _thread;
/// <summary>
/// Creates a timer with 1 [ms] interval
/// </summary>
public HighResolutionTimer() : this(1f)
{
}
/// <summary>
/// Creates timer with interval in [ms]
/// </summary>
/// <param name="interval">Interval time in [ms]</param>
public HighResolutionTimer(float interval)
{
Interval = interval;
}
/// <summary>
/// The interval of a timer in [ms]
/// </summary>
public float Interval
{
get => _interval;
set
{
if (value < 0f || Single.IsNaN(value))
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_interval = value;
}
}
/// <summary>
/// True when timer is running
/// </summary>
public bool IsRunning => _isRunning;
/// <summary>
/// If true, sets the execution thread to ThreadPriority.Highest
/// (works after the next Start())
/// </summary>
/// <remarks>
/// It might help in some cases and get things worse in others.
/// It suggested that you do some studies if you apply
/// </remarks>
public bool UseHighPriorityThread { get; set; } = false;
/// <summary>
/// Starts the timer
/// </summary>
public void Start()
{
if (_isRunning) return;
_isRunning = true;
_thread = new Thread(ExecuteTimer)
{
IsBackground = true,
};
if (UseHighPriorityThread)
{
_thread.Priority = ThreadPriority.Highest;
}
_thread.Start();
}
/// <summary>
/// Stops the timer
/// </summary>
/// <remarks>
/// This function is waiting an executing thread (which do to stop and join.
/// </remarks>
public void Stop(bool joinThread = true)
{
_isRunning = false;
// Even if _thread.Join may take time it is guaranteed that
// Elapsed event is never called overlapped with different threads
if (joinThread && Thread.CurrentThread != _thread)
{
_thread.Join();
}
}
private void ExecuteTimer()
{
float nextTrigger = 0f;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
while (_isRunning)
{
nextTrigger += _interval;
double elapsed;
while (true)
{
elapsed = ElapsedHiRes(stopwatch);
double diff = nextTrigger - elapsed;
if (diff <= 0f)
break;
if (diff < 1f)
Thread.SpinWait(10);
else if (diff < 5f)
Thread.SpinWait(100);
else if (diff < 15f)
Thread.Sleep(1);
else
Thread.Sleep(10);
if (!_isRunning)
return;
}
double delay = elapsed - nextTrigger;
Elapsed?.Invoke(this, new HighResolutionTimerElapsedEventArgs(delay));
if (!_isRunning)
return;
// restarting the timer in every hour to prevent precision problems
if (stopwatch.Elapsed.TotalHours >= 1d)
{
stopwatch.Restart();
nextTrigger = 0f;
}
}
stopwatch.Stop();
}
private static double ElapsedHiRes(Stopwatch stopwatch)
{
return stopwatch.ElapsedTicks * TickLength;
}
}
public class HighResolutionTimerElapsedEventArgs : EventArgs
{
/// <summary>/// Real timer delay in [ms]/// </summary>
public double Delay { get; }
internal HighResolutionTimerElapsedEventArgs(double delay)
{
Delay = delay;
}
}
}
@acidos
Copy link

acidos commented Jul 6, 2023

@DraTeots thanks for the class.
How can I call async methods from the Elapsed handler?

@DraTeots
Copy link
Author

DraTeots commented Jul 7, 2023

Technically you can do this. But this might be purely perpendicular to this class intent. The HighResolutionTimer class is designed to avoid overlapping Elapsed events. Calling an asynchronous method without awaiting it within the Elapsed event handler can lead to concurrent operations, which breaks this design intent. If an async operation is longer than the timer interval, this could result in multiple concurrent Elapsed events. Also, unhandled exceptions in the async operation could crash the process unless caught within the event handler. If the operations you need to perform are inherently asynchronous and you want to maintain the "non-overlapping" behavior, you may need to significantly redesign this timer class.

But in my experience, usually if one needs something like high precision timer, one is in a realm of pseudo real-time application where any async in real-time code part brings undesirable uncertainties and complications. That thoughts dictated this class design.

Calling async:

timer.Elapsed += async (sender, args) =>
{
    // This will be started, but not waited for. So if the timer triggers again, 
    // and the previous execution isn't done, you'll have two (or more) of these 
    // running at the same time, each on their own thread.
    await SomeAsyncMethod();
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment