Timers in .NET

A current C# project of mine required a timer where every couple of seconds a method would fire and a potentially fairly long-running process would run.

With .NET we have a few built-in options for timers:

System.Web.UI.Timer

Available in the .NET Framework 4.8 which performs asynchronous or synchronous Web page postbacks at a defined interval and was used back in the older WebForms days.

System.Windows.Forms.Timer

This timer is optimized for use in Windows Forms applications and must be used in a window.

System.Timers.Timer

Generates an event after a set interval, with an option to generate recurring events. This timer is almost what I need however this has quite a few stackoverflow posts where exceptions get swallowed.

System.Threading.Timer

Provides a mechanism for executing a method on a thread pool thread at specified intervals and is the one I decided to go with.

Issues

I came across a couple of minor issues the first being that even though I held a reference to my Timer object in my class and disposed of it in a Dispose method the timer would stop ticking after a while suggesting that the garbage collector was sweeping up and removing it.

My Dispose method looks like the first method below and I suspect it is because I am using the conditional access shortcut feature from C# 6 rather than explicitly checking for null first.

public void Dispose()
{ 
	// conditional access shortcut
	_timer?.Dispose(); 
} 

public void Dispose()
{ 
	// null check
	if(_timer != null)
	{
		_timer.Dispose(); 
	}
} 

A workaround is to tell the garbage collector to not collect this reference by using this line of code in timer’s elapsed method.

GC.KeepAlive(_timer);

The next issue was that my TimerTick event would fire and before the method that was being called could finish another tick event would fire.

This required a stackoverflow search where the following code fixed my issue.

// private field
private readonly object _locker = new object();

// this in TimerTick event
if (Monitor.TryEnter(_locker))
{
	try
	{
		// do long running work here
		DoWork();
	}
	finally
	{
		Monitor.Exit(_locker);
	}
}

And so with these two fixes in place, my timer work was behaving as expected.

Solution

Here is a sample class with the above code all in context for future reference

Success 🎉