Created
November 18, 2019 13:53
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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