tabs ↹ over ␣ ␣ ␣ spaces

by Jiří {x2} Činčura

Scheduling absolute time operations with Timer

23 Oct 2013 4 mins Multithreading/Parallelism/Asynchronous/Concurrency, Reactive Extensions (Rx)

Sometimes I’m coding a simple task – start an operation at a given time. Some kind of scheduler in inside application. No, I’m not going to cry over Thread and Thread.Sleep abuse. I’m over it. 😃 It’s Timer usage.

That’s of course way better than above mentioned abuse. But let’s say you want to run the operation every day at 14:00 (that’s 2PM for some 😉). The code is often like this (or close to it).

var timer = new Timer(_ =>
{
	var now = DateTimeOffset.UtcNow; // or maybe in your local time
	if (now.Hour == 14 && now.Minute == 0)
	{
		// ...
	}
}, null, TimeSpan.Zero, TimeSpan.FromMinute(1));

Pretty simple, right? It’s even not wrong. Ticking every minute will probably have small impact on the system (thread pool) and very often it will run almost no code. But it can be done better.

When you’re starting the ticking you can compute when the next tick should be, right? Same as when you finish (or start) the operation. You just need to reschedule. The rescheduling might be tricky, but you actually can move the Timer forward (I often refer to this on my courses as “kick” or “kicking the timer” using Change method. Whenever you want. It’s something that might not be immediately obvious. Let’s see the code.

var timer = default(Timer);
timer = new Timer(o =>
{
	try
	{
		callback(o);
	}
	finally
	{
		timer.Change(ComputeNext(), Timeout.InfiniteTimeSpan);
	}
}, null, ComputeNext(), Timeout.InfiniteTimeSpan);

I just need to first declare the variable because I’ll use it in the lambda/delegate. Then I simply compute the interval (TimeSpan) between “now” and the date/time the operation should happen. With that I have a Timer instance that will tick just once. But at the end (you can to it even as a first step) I’ll “kick” it forward and I’m done. It’s similar trick as if you’d like to take into account how long the method’s execution took.

And that’s it. I created a simple helper for it, so you can just grab it and use it. Or change for different intervals (not daily). I also created one overload for async methods, because else the behavior would not be correct (TimerCallback is basically Action<object> and hence void returning method aka you cannot await it).

Also if you’re using Rx (Reactive Extensions) you can use this overload (with IScheduler) of Observable.Timer to do the same (maybe using more succinct code).

static class DailyHourMinuteTimerHelper
{
	public static Timer Create(int hour, int minute, Func<DateTimeOffset> nowFactory, TimerCallback callback, object state)
	{
		return Create(hour, minute, nowFactory, o => { callback(o); return Task.FromResult<object>(null); }, state);
	}

	public static Timer Create(int hour, int minute, Func<DateTimeOffset> nowFactory, Func<object, Task> callback, object state)
	{
		var timer = default(Timer);
		timer = new Timer(async o =>
		{
			try
			{
				await callback(o);
			}
			finally
			{
				timer.Change(FixTimer(ComputeDueTime(nowFactory, hour, minute)), Timeout.InfiniteTimeSpan);
			}
		}, state, FixTimer(ComputeDueTime(nowFactory, hour, minute)), Timeout.InfiniteTimeSpan);
		return timer;
	}

	static TimeSpan FixTimer(TimeSpan timeSpan)
	{
		// on these long intervals the Timer drifts a little (probably because time corrections when synchronized from NTP)
		// but because I'm on a minute precision I don't care a second
		return timeSpan.Add(TimeSpan.FromSeconds(1));
	}

	static TimeSpan ComputeDueTime(Func<DateTimeOffset> nowFactory, int hour, int minute)
	{
		return ComputeNext(nowFactory(), hour, minute) - nowFactory();
	}

	static DateTimeOffset ComputeNext(DateTimeOffset now, int hour, int minute)
	{
		var next = new DateTimeOffset(now.Year, now.Month, now.Day, hour, minute, 0, now.Offset);
		if (next <= new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0, now.Offset))
			next = next.AddDays(1);
		return next;
	}
}

Profile Picture Jiří Činčura is .NET, C# and Firebird expert. He focuses on data and business layers, language constructs, parallelism, databases and performance. For almost two decades he contributes to open-source, i.e. FirebirdClient. He works as a senior software engineer for Microsoft. Frequent speaker and blogger at www.tabsoverspaces.com.