TaskTimer: how to execute task on a timer

Over the week-end I created the TaskTimer class that allows to execute code on timer in an async method:

static async Task DoStuffOnTimer()
{
    using (var timer = new TaskTimer(1000).Start())
    {
        foreach (var task in timer)
        {
            await task;
            DoStuff();
        }
    }
}

Source code: https://github.com/ikriv/tasktimer

Nuget package: https://www.nuget.org/packages/TaskTimer/1.0.0

Motivation

To my astonishment, I have found that .NET Task Library does not (seem to) have support for executing a task on timer, similar to Observable.Interval(). I needed to execute an activity every second in my async method, and the suggestions I found on StackOverflow were quite awful: from using Task.Delay() to using a dedicated thread and Thread.Sleep().

Task.Delay() is good enough in many cases, but it has a problem. Consider this code:

static async Task UseTaskDelay()
{
    while (true)
    {
        DoSomething(); // takes 300ms on average
        await Task.Delay(1000);
    }
}

One execution of the loop takes 1300ms on average, instead of  1000ms we wanted. So, on every execution we will accumulate an additional delay of 300ms. If DoSomething() takes different time from iteration to iteration, our activity will not only be late, but it will also be irregular. One could, of course, use real-time clock to adjust the delay after every execution, but this is quite laborious and hard to encapsulate.

Using TaskTimer

Browser for “TaskTimer” package on NuGet and add it to your project. Alternatively, you can get the code from GitHub. The code requires .NET 4.5 or above.

You will need to choose the timer period, and the initial delay, or absolute start time:

// period=1000ms, starts immediately
var timer = new TaskTimer(1000).Start();

// period=1 second, starts immediately
var timer = new TaskTimer(TimeSpan.FromSeconds(1)).Start();

// period=1000ms, starts 300ms from now
var timer = new TaskTimer(1000).Start(300);

// period=1000ms, starts 5 minutes from now
var timer = new TaskTimer(1000).Start(TimeSpan.FromMinutes(5));

// period=1000ms, starts at next UTC midnight
var timer = new TaskTimer(1000).StartAt(DateTime.UtcNow.Date.AddDays(1));

The returned object is a disposable enumberable. You must call Dispose() on it to properly disposer of the underlying timer object. The enumerable iterates over an infinite series of tasks.  Task number N becomes completed after the timer ticks N times.  The enumerable can be enumerated only once.

Avoiding infinite loop

The enumerable returned by the timer is infinite, so you cannot get all the task at once.

// won't work; this would require an array of infinite length
var tasks = timer.ToArray();

You can use foreach on the timer, but that would be an infinite loop:

using (var timer = new TaskTimer(1000).Start())
{
    // infinite loop
    foreach (var task in timer)
    {
        await task;
        DoSomethingUseful();
    }
}

If you know the number of iterations in advance, use Take() method:

// loop executes 10 times
foreach (var task in timer.Take(10))
{
    await task;
    DoSomethingUseful();
}

If you don’t, you can use a cancellation token:

async Task Tick(CancellationToken token)
{
    using (var timer = new TaskTimer(1000).CancelWith(token).Start())
    {
        try
        {
            foreach (var task in timer)
            {
                await task;
                DoSomethingUseful();
            }
        }
        catch (TaskCanceledException)
        {
            Console.WriteLine("Timer Canceled");
        }
    }
}

About Disposable Enumerable

I initially implemented the timer sequence as generator method using the yield keyword. You can see this in the GitHub history:

public IEnumerable<Task> StartOnTimer(...)
{
    using (var taskTimer = new TaskTimerImpl(...))
    {
        while (true)
        {
            yield return taskTimer.NextTask();
        }
    }
}

The trouble with this approach was that calling the Start() method did not actually start the timer: the timer would be started only when one begins to iterate over it. Furthermore, each iteration will create a new timer. I found this too confusing. I had enough trouble understanding similar behavior in observables to inflict this on the users of my own class.

So, I decided to switch to a schema where the timer gets started immediately upon calling of the Start() method. In reactive terms, TaskTimer is a hot observable. To stop the timer, one needs to dispose the return value of the Start() method, hence the disposable enumerable. To use the timer properly, one should put a foreach loop inside a using block like in the examples, above.

Also, it is only possible to iterate over the timer once. The following will not work:

using (var timer = new TaskTimer(1000).Start())
{
    // loop executes 10 times
    foreach (var task in timer.Take(10))
    {
        await task;
        DoSomethingUseful();
    }

    // execute 20 more times -- WON'T WORK! Throws InvalidOperationException!
    foreach (var task in timer.Take(20)) ...
}

It should be possible to implement a timer with multiple iteration semantics, but I decided it was not worth the complexity.

Conclusion

It’s a shame Task Library does not provide timer object out of the box. I hope I was able to fill the gap, at least to some extent. In the process I ran into interesting problems with the way IEnumerable, IEnumerator and IDisposable intersect, this is a fascinating world. I hope you do enjoy the result!

Leave a Reply

Your email address will not be published. Required fields are marked *