Skip to content

Instantly share code, notes, and snippets.

@radleta
Created November 18, 2019 13:53
Show Gist options
  • Save radleta/1ddb2b350fff65aea2cbe8c2089cc4d4 to your computer and use it in GitHub Desktop.
Save radleta/1ddb2b350fff65aea2cbe8c2089cc4d4 to your computer and use it in GitHub Desktop.
A thread safe wrapper for the Timer to ensure only one callback is executing at a time. This prevents long running callbacks from overlapping their execution. Also, provides a wrapper to ensure exceptions are properly logged when they are thrown from the callback.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace RichardAdleta
{
/// <summary>
/// A thread safe wrapper for the <see cref="Timer"/> to ensure only one callback is executing at a time.
/// This prevents long running callbacks from overlapping their execution.
/// Also, provides a wrapper to ensure exceptions are properly logged when they are thrown from the callback.
/// </summary>
public class AsyncTimer : IDisposable
{
private bool disposedValue = false; // To detect redundant calls
private readonly Timer _timer;
private readonly object _syncLock = new object();
private bool _timerRunning = false;
private Func<object, CancellationToken, Task> _asyncCallback;
private int _timeout;
private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
public Timer Timer => _timer;
/// <summary>
/// Initializes a new instance of this class.
/// </summary>
/// <param name="asyncCallback">Thread Safe. The callback on each interval. Only one call at a time will happen on this callback.</param>
/// <param name="state">The state to pass to the callback.</param>
/// <param name="dueTime">The time to initially delay the first callback. See <see cref="Timer"/> documentation on dueTime.</param>
/// <param name="period">The interval to call the callback. If the previous callback is still running, the next interval will be skipped.</param>
/// <param name="timeout">The timeout of the cancellation token to be passed to the action. Less than one equals no timeout.</param>
public AsyncTimer(Func<object, CancellationToken, Task> asyncCallback, object state, long dueTime, long period, int timeout)
{
_asyncCallback = asyncCallback ?? throw new ArgumentNullException(nameof(asyncCallback));
_timer = new Timer(TimerCallback, state, dueTime, period);
_timeout = timeout;
}
/// <summary>
/// The callback for the timer.
/// </summary>
/// <param name="state">The state.</param>
private void TimerCallback(object state)
{
lock (_syncLock)
{
if (_timerRunning
|| disposedValue)
{
return;
}
else
{
_timerRunning = true;
}
}
try
{
// spawn a task to do the work so it can be async
Task.Run(async () =>
{
try
{
using (var cts = _timeout > 0 ? new CancellationTokenSource(_timeout) : new CancellationTokenSource())
using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, _disposeCancellationTokenSource.Token))
{
await _asyncCallback(state, linkedCts.Token).ConfigureAwait(false);
}
}
catch (Exception ex)
{
TelemetryClientManager.Default.TrackException(ex);
}
finally
{
// make sure we're all done
lock (_syncLock)
{
_timerRunning = false;
}
}
});
}
catch (Exception ex)
{
TelemetryClientManager.Default.TrackException(ex);
}
}
#region IDisposable Support
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
_timer.Dispose();
_disposeCancellationTokenSource.Dispose();
disposedValue = true;
}
}
~AsyncTimer()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(false);
}
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment