Monday, July 18, 2011

Task Parallel Library: How To Write a Simple Delay Task

I just had a need for a delay task. A simple method that I can call to create a task that will turn a Func<T> into a Task<T> that will execute after a given delay.

The starting point for any Task creation based on an external asynchronous operation, like a Timer callback, is the TaskCompletionSource class.  It provides methods to transition the task it creates to different states. You call SetResult when the operation is completes, SetException if the operation fails, and SetCancelled if you want to cancel the task.

Here’s my RunDelayed method:

private static Task<T> RunDelayed<T>(int millisecondsDelay, Func<T> func)
{
if (func == null)
{
throw new ArgumentNullException("func");
}
if (millisecondsDelay < 0)
{
throw new ArgumentOutOfRangeException("millisecondsDelay");
}

var taskCompletionSource = new TaskCompletionSource<T>();

var timer = new Timer(self =>
{
((Timer)self).Dispose();
try
{
var result = func();
taskCompletionSource.SetResult(result);
}
catch (Exception exception)
{
taskCompletionSource.SetException(exception);
}
});
timer.Change(millisecondsDelay, millisecondsDelay);

return taskCompletionSource.Task;
}

I simply create a new TaskCompletionSource and a Timer where the callback calls SetResult with the result of the given Func<T>. If the Func<T> throws, we simply catch the exception and call SetException. Finally we start the timer and return the Task.

You would use it like this:

var task = RunDelayed(1000, () => "Hello World!");
task.ContinueWith(t =>
{
// 'Hello World' is output a second later on a threadpool thread.
Console.WriteLine(t.Result);
});

You can use the same technique to turn any asynchronous operation into a Task.

Note however if your operation exposes an APM API, it’s much easier to use the Task.Factory.FromAsync method.

6 comments:

Unknown said...
This comment has been removed by the author.
Unknown said...

How about using new System.Threading.EventWaitHandle(false, EventResetMode.ManualReset).WaitOne(millisecondsDelay) instead of Timer

Mike Hadlow said...

Hi Koistya,

What benefit would you get from using EventWaitHandle?

Richard OD said...

It is useful to note there is something similar to this in the Parallel Programming with .NET examples extension methods for TaskFactory.

Download is here- http://archive.msdn.microsoft.com/ParExtSamples/Release/ProjectReleases.aspx?ReleaseId=4179

Ken Egozi said...

A couple of subtle bugs here:

1. you should pass -1 as the second argument to Change to avoid a possibility of the event fire multiple times (when all threads are taken), then the tcs would throw on the SetException call.

2. even more subtle - the timer itself is not rooted anywhere so potentially be garbage collected before fired, then the task would never complete. the easiest fix is to add ContinueWith to the tcs.Task, pass in a closure that closes on the timer object (thus keeps reference to it), and dispose of it then.

Mike Hadlow said...

Thanks for that Ken. I'll update the code.