I'm trying to create a precision timer. I found an example created with WinMM.dll. The sample works really fine. But it crashes with the first garbage collector.
How can I prevent the garbage collector from blocking the timer?
public class WinMMWrapper : IDisposable
{
[DllImport("WinMM.dll", SetLastError = true)]
public static extern uint timeSetEvent(int msDelay, int msResolution,
TimerEventHandler handler, ref int userCtx, int eventType);
[DllImport("Winmm.dll", CharSet = CharSet.Auto)] // <=== ADD THIS
static extern uint timeKillEvent(uint uTimerID); // <=== ADD THIS
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 uint _timerId; // <=== ADD THIS
private bool _disposed; // <=== ADD THIS
public WinMMWrapper(int elapsedMs, int resolutionMs, TimerEventType timerEventType, Action elapsedAction)
{
_elapsedMs = elapsedMs;
_resolutionMs = resolutionMs;
_timerEventType = timerEventType;
_elapsedAction = elapsedAction;
}
public bool StartElapsedTimer() // <=== RETURN bool
{
StopTimer(); // Stop any started timer
int myData = 1;
// === SET _timerId
_timerId = timeSetEvent(_elapsedMs, _resolutionMs / 10, new TimerEventHandler(TickHandler), ref myData, (int)_timerEventType);
return _timerId != 0;
}
public void StopTimer() // <=== ADD THIS
{
if (_timerId != 0)
{
timeKillEvent(_timerId);
_timerId = 0;
}
}
private void TickHandler(uint id, uint msg, ref int userctx, int rsv1, int rsv2)
{
_elapsedAction();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed && disposing)
StopTimer();
_disposed = true;
}
~WinMMWrapper()
{
Dispose(false);
}
}
My Static Class
public static class Global
{
public static WinMMWrapper timer;
}
Create WinMMWrapper
private void TimerStart_Click(object sender, RoutedEventArgs e)
{
Global.timer = new WinMMWrapper(1, 1, WinMMWrapper.TimerEventType.Repeating, Tick);
Global.timer.StartElapsedTimer();
}
Tick Function
private static void Tick()
{
Console.WriteLine("Time : " + DateTime.Now.ToString("hh:mm:ss:ffff"));
}
Error Message
Managed Debugging Assistant 'CallbackOnCollectedDelegate' : A callback was made on the garbage-collected delegate of type 'CanBusRandomDataGenerator!CanBusRandomDataGenerator.WinMMWrapper+TimerEventHandler::Invoke'. This can cause app crashes, corruption, and data loss. When delegating to unmanaged code, it must be kept alive by the managed application until it is guaranteed that the delegates will never be called.'
The code is now exactly the same. It works for about 2 3 seconds, then it crashes to the following error. Error occurs within WinMMWrapper function without falling into Dispose.
You must keep the timer
variable alive as long as you are using the timer. If it is a local variable, it will be reclaimed by the GC when you leave the method. Do so by converting this local variable to a class field (possibly static). In a Console application you can still use a local variable, but you must add a Console.ReadKey();
to prevent the application to exit prematurely.
Also, stop the timer before this variable becomes eligible for garbage collection. To do so, let WinMMWrapper
implement IDisposable
.
Make sure that the object where the callback Action lives stays alive and is not disposed! Probably this is the object where you call new WinMMWrapper(..., theAction)
.
public class WinMMWrapper : IDisposable
{
[DllImport("WinMM.dll", SetLastError = true)]
public static extern uint timeSetEvent(int msDelay, int msResolution,
TimerEventHandler handler, ref int userCtx, int eventType);
[DllImport("Winmm.dll", CharSet = CharSet.Auto)] // <=== ADD THIS
static extern uint timeKillEvent(uint uTimerID); // <=== ADD THIS
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 iuint _timerId; // <=== ADD THIS
private bool _disposed; // <=== ADD THIS
public WinMMWrapper(int elapsedMs, int resolutionMs, TimerEventType timerEventType, Action elapsedAction)
{
_elapsedMs = elapsedMs;
_resolutionMs = resolutionMs;
_timerEventType = timerEventType;
_elapsedAction = elapsedAction;
}
public bool StartElapsedTimer() // <=== RETURN bool
{
Stop(); // Stop any started timer
int myData = 1;
// === SET _timerId
_timerId = timeSetEvent(_elapsedMs, _resolutionMs / 10, new TimerEventHandler(TickHandler), ref myData, (int)_timerEventType);
return _timerId != 0;
}
public void StopTimer() // <=== ADD THIS
{
if (_timerId != 0)
{
timeKillEvent(_timerId);
_timerId = 0;
}
}
private void TickHandler(uint id, uint msg, ref int userctx, int rsv1, int rsv2)
{
_elapsedAction();
}
// === ADD Dispose and finalizer ===
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed && disposing)
StopTimer();
}
_disposed = true;
}
~MMTimer()
{
Dispose(false);
}
}
Then you can do this in a Console application:
using (var timer = new WinMMWrapper(1, 1, WinMMWrapper.TimerEventType.Repeating,
() => Console.WriteLine("Time : " + DateTime.Now.ToString("hh:mm:ss:fff"))) {
Console.Writeline("Hit a key to stop the timer and quit the application!");
Console.ReadKey();
} // <= Here timer.Dispose() gets automatically called by using.
If you cannot use a using statement because your timer will be stopped at another place in your code, you can also call timer.Dispose();
explicitly.
To make this code thread-safe, enclose your start and stop timer code in a lock(this { ... }
statement.