.NET – Be wary of System.Timers.Timer

I’m working on large project that I will be releasing soon enough. I started it back in 2015 and am finally getting around to finishing it this year! I’ve rewritten a lot of the code so far and found that the reason for one of the major bugs was Microsoft’s System.Timers.Timer. It can and has caused a bizarre UI-lock and memory leak pattern that I’ve repeated consistently with the same results over and over. It will also cause things like a NetworkStream to read bytes from the wrong thread. If you think I’m joking about this, I’m not. I saw it repeatedly. So I made a drop-in replacement timer class that was heavily based on System.Timers.Timer from what I saw in dotPeek. I made my own adjustments, mostly for things that weren’t needed, and not only has it not had this locking issues, but it has no memory leak issues either. Nor oddities like reading bytes from the wrong thread (I legitimately don’t know how it was doing that). I will post it here for those wanting it. Eventually it will be released as part of this entire project.

Code is below (click “Read the rest of this entry ยป” button if not on page)

NTimer.cs

namespace NRGsoft.WordChainGame.ServerClientShared
{
	using System;
	using System.Threading;
	using System.Runtime.InteropServices;
	using System.Runtime.Versioning;
	using System.Security;
 
	/// <summary>
	/// NTimer is a drop-in replacement for System.Timer Timers that need to be
	/// independent from the UI.
	/// </summary>
	public class NTimer
	{
		protected Object cookie; // Blame MS for this naming convention
		protected bool enabled = false;
		protected Timer timer;
		protected int interval = 100;
		public NTimerElapsedEventHandler Elapsed;
 
		public bool AutoReset { get; set; } = true;
 
		public bool Enabled
		{
			get => enabled;
 
			set
			{
				if (value && !enabled)
					StartNewTimer();
				else if (!value && enabled)
					StopAndClearTimer();
 
				enabled = value;
			}
		}
 
		public int Interval
		{
			get => interval;
 
			set
			{
				if (value == interval)
					return;
 
				interval = value;
				UpdateTimer();
			}
		}
 
		public NTimer(int interval)
		{
			this.interval = interval;
		}
 
		private void TimerCallback(object state)
		{
			// prevent firing off event if it's disabled
			if (state != cookie)
				return;
 
			if (!AutoReset)
				Enabled = false;
 
			var filetime = new FILETIME();
			GetSystemTimeAsFileTime(ref filetime);
			Elapsed?.Invoke(this,
				new NTimerElapsedEventArgs(filetime.dwLowDateTime, filetime.dwHighDateTime)
			);
		}
 
		public void Start()
		{
			Enabled = true;
		}
 
		public void Stop()
		{
			Enabled = false;
		}
 
		public void StartDelayed(int delayBy)
		{
			this.enabled = true;
			StartNewTimer(delayBy + interval);
		}
 
		private void StartNewTimer()
		{
			cookie = new Object();
			ClearTimer();
			timer = new Timer(TimerCallback, cookie, interval, AutoReset ? interval : -1);
		}
 
		private void StartNewTimer(int delayTime)
		{
			cookie = new Object();
			ClearTimer();
			timer = new Timer(TimerCallback, cookie, delayTime, AutoReset ? interval : -1);
		}
 
		protected void UpdateTimer()
		{
			timer?.Change(interval, AutoReset ? interval : -1);
		}
 
		private void StopAndClearTimer()
		{
			cookie = null;
			ClearTimer();
		}
 
		private void ClearTimer()
		{
			timer?.Dispose();
			timer = null;
		}
 
 
		[StructLayout(LayoutKind.Sequential)]
		internal struct FILETIME
		{
			internal int dwLowDateTime;
			internal int dwHighDateTime;
		}
 
		[ResourceExposure(ResourceScope.None)]
		[DllImport("kernel32.dll"), SuppressUnmanagedCodeSecurity()]
		internal static extern void GetSystemTimeAsFileTime(ref FILETIME lpSystemTimeAsFileTime);
	}
}

NTimerElapsedEventArgs.cs

namespace NRGsoft.WordChainGame.ServerClientShared
{
	using System;
 
	public class NTimerElapsedEventArgs : EventArgs
	{
		public NTimerElapsedEventArgs(int low, int high)
		{
			SignalTime = DateTime.FromFileTime((long)high << 32 | (low & 0xFFFFFFFF));
		}
 
		public DateTime SignalTime { get; }
	}
}

NTimerElapsedEventHandler.cs

namespace NRGsoft.WordChainGame.ServerClientShared
{
	public delegate void NTimerElapsedEventHandler(object sender, NTimerElapsedEventArgs e);
}
 
 
© 2017 NRGsoft