Every so often I come across the need for some simple code to schedule a bit of work in the background of an application. Sometimes because a service (for integration tasks, perhaps) needs to kick off processing every so often, or sometimes because some background part of a larger program needs to happen in parallel with the main execution. A common part of these requirements is that the task should run every so often, but two instances of the task should not overlap no matter how long the background processing takes.
A few times I’ve come across projects with subtly broken implementations of this requirement, so I thought I’d write down a simple approach that has worked for me. That way next time I need it, I won’t have to go digging through git repos…
Running some code every so often implies we need something to run, and a timescale to run it on. So the beginnings of a class to handle this might look like:
using System;
using System.Threading;
public class FunctionScheduler
{
private int _runEveryMs;
private Action _actionToRun;
public FunctionScheduler(int runEveryMs, Action actionToRun)
{
_runEveryMs = runEveryMs;
_actionToRun = actionToRun;
}
}
Then we need to be able to start the process, and schedule the action. That’s simple enough with the standard Windows timer APIs:
private Timer _timer = null;
public void Start()
{
_timer = new Timer(new TimerCallback(timerTick), null, 0, _runEveryMs);
}
private void timerTick(object o)
{
_actionToRun?.Invoke();
}
So every time the Timer
ticks, the action will get called. (Now there’s more that you could do here to cover cases like “what happens if Start()
is called twice” – but you get the idea)
So the next step is to consider that requirement to ensure that the action is always allowed to finished executing before it is invoked again. This is where I’ve seen a lot of code fall down in solutions in the past – implementing complex code to try and achieve this.
But there’s an easy solution in the Windows APIs. A simple thread-safe way of checking the state of the action is the Monitor
object. This can “lock” an object in our code. If the lock is taken out when he action starts and released when the action ends, we have a simple and safe way of ensuring the action is never run in parallel with itself.
private object _lock = new object();
private void timerTick(object o)
{
if (Monitor.TryEnter(_lock))
{
_actionToRun?.Invoke();
Monitor.Exit(_lock);
}
}
So if the timer fires while the action is still running, then it will not be able to acquire the lock, and hence a second instance of the action won’t be invoked.
Now there’s a flaw here – what happens if the action breaks? Well if an exception gets thrown during the action execution but your app doesn’t exit then the Monitor
stays in the “entered” state. Once that happens, each time the Timer
triggers the action won’t get re-run. But of course it’s simple enough to fix this:
private void timerTick(object o)
{
if (Monitor.TryEnter(_lock))
{
try
{
_actionToRun?.Invoke();
}
finally
{
Monitor.Exit(_lock);
}
}
}
Now, no matter what happens with the action, once it completes the Monitor
lock gets released.
And finally, we can stop the timer when it’s no longer necessary:
public void Stop()
{
_timer.Dispose();
}
And with that, we can have our action triggered on schedule… I’m not sure whether this is “The Best” way to achieve this – but this has been scheduling the main update loop in my homebrew RSS reader for about five years now, and seems to work reliably. There’s a gist of the full class if you want to copy it.
That approach makes most sense when you have a single bit of work that’s scheduled for the lifetime of your process, but sometimes you have smaller tasks which you need to start and stop more often. In that scenario the code might be more readable with a dispose-based pattern:
public class DisposableFunctionScheduler : IDisposable
{
private int _runEveryMs;
private Action _actionToRun;
private Timer _timer = null;
private object _lock = new object();
public DisposableFunctionScheduler(int runEveryMs, Action actionToRun)
{
_runEveryMs = runEveryMs;
_actionToRun = actionToRun;
_timer = new Timer(new TimerCallback(timerTick), null, 0, _runEveryMs);
}
public void Dispose()
{
_timer.Dispose();
}
private void timerTick(object o)
{
if (Monitor.TryEnter(_lock))
{
try
{
_actionToRun?.Invoke();
}
finally
{
Monitor.Exit(_lock);
}
}
}
}
So you can schedule some work as follows:
class Program
{
static void Action()
{
// Do something useful here...
Console.WriteLine("Action called at " + DateTime.Now.ToString());
}
static void Main(string[] args)
{
Console.WriteLine("Starting...");
using (var fs = new DisposableFunctionScheduler(4000, Action))
{
// Do whatever other work needs to happen while
// the scheduler works in the background...
Console.ReadLine();
}
Console.WriteLine("Stopping...");
Console.ReadLine();
}
}
Simple – and now documented for the next time I realise I need this code…
Top comments (0)