I've created a System.Threading.Timer
wrapper in C#. The tasks to be triggered when the timer elapses have highly variable execution times. My design criteria are:
- Provide a strongly typed timer state in the callback
- Run the timer callback on a periodic basis
- If the timer's period elapses before the timer callback completes
- Do not run the timer callback a second time in parallel
- Run time timer callback again immediately after the running callback completes
- Do not run the timer callback more than once per period
As an example, given a timer that elapses every 3 seconds:
3s - Timer elapse triggers task
6s - Timer elapse triggers task, but task does not run because
the previous run is not complete
9s - Timer elapse triggers task, but task does not run because
the previous run is not complete
10s - Task finishes running
10s - Task immediately runs again because the previous run crossed
a period boundary
11s - Task finishes running
12s - Timer elapse triggers task
I ended up with the following class:
public class NonOverrunningTimer<TState>
{
private readonly Func<TState, TState> _delegate;
private readonly TimeSpan _interval;
private readonly System.Threading.Timer _timer;
public NonOverrunningTimer(Func<TState, TState> @delegate, TimeSpan interval, TState initialState = default(TState))
{
if (interval == TimeSpan.Zero)
{
throw new Exception($"Cannot initialize a timer with a period of {TimeSpan.Zero}");
}
State = initialState;
_delegate = @delegate;
_interval = interval;
_timer = new System.Threading.Timer(UntypedCallback, null, TimeSpan.Zero, interval);
}
public TState State { get; private set; }
private volatile bool _running;
private volatile bool _waiting;
private readonly object _executeLock = new object();
public void UntypedCallback(object state)
{
if (_running && _waiting)
{
return;
}
if (_waiting && !_running)
{
_waiting = false;
}
if (_running)
{
_waiting = true;
return;
}
using (var @lock = new TryLock(_executeLock))
{
if ([email protected])
{
return;
}
_running = true;
State = _delegate(State);
_running = false;
if (_waiting)
{
_waiting = false;
_timer.Change(TimeSpan.Zero, _interval);
}
}
}
private class TryLock : IDisposable
{
private object _locked;
public bool HasLock { get; private set; }
public TryLock(object obj)
{
if (Monitor.TryEnter(obj))
{
HasLock = true;
_locked = obj;
}
}
public void Dispose()
{
if (!HasLock)
{
return;
}
Monitor.Exit(_locked);
_locked = null;
HasLock = false;
}
}
}
I'm interested in general feedback as well as whether something like this already exists in the BCL/Framework libraries and I just missed it.