Crafting a Task.TimeoutAfter Method

Imagine that you have a Task handed to you by a third party, and that you would like to force this Task to complete within a specified time period. However, you cannot alter the “natural” completion path and completion state of the Task, as that may cause problems with other consumers of the Task. So you need a way to obtain a copy or “proxy” of the Task that will either (A) complete within the specified time period, or (B) will complete with an indication that it had timed out.

In this blog post, I will show how one might go about implementing a Task.TimeoutAfter method to support this scenario. The signature of TimeoutAfter would look like this:

Simple enough, right? You start a Timer job that faults the proxy Task, and also add a continuation off of the source Task that transfers the completion state of the source to the proxy. The final state of the proxy will therefore depend on which completes first, the Timer job or the source Task.

The “RanToCompletion” handling might seem a little more complicated than it needs to be, but it will allow us to handle Task<TResult> objects correctly (discussed briefly below).

Can We Do Better?

While our first stab at a TimeoutAfter method is functionally correct, we could streamline it and improve its performance. Specifically, notice that our Timer and continuation delegates “capture” variables; this will cause the compiler to allocate special “closure” classes for these delegates behind the scenes, which will slow down our method. To eliminate the need for the closure class allocations, we can pass all “captured” variables in through state variables for those respective calls, like this:

Some ad-hoc performance tests show that this little optimization shaves about 12% off of the overhead from the TimeoutAfter method.

What about Edge Cases?

What do we do when the caller specifies a zero timeout, or an infinite timeout? What if the source Task has already completed by the time that we enter the TimeoutAfter method? We can address these edge cases in the TimeoutAfter implementation as follows:

The implementation above takes advantage of the new async/await support in .NET 4.5, and is pleasingly concise. However, it does lack some optimizations:

The edge cases described previously are not handled well. (But that could probably be fixed.)

A Task is created via Task.Delay, instead of just a simple timer job.

In the cases where the source Task (task) completes before the timeout expires, no effort is made to cancel the internal timer job that was launched in the Task.Delay call. If the number of “zombie” timer jobs starts becoming significant, performance could suffer.

Nevertheless, it is good to consider the use of async/await support in implementing features like this. Often await will be optimized in ways that simple continuations are not.

What about TimeoutAfter<TResult>?

Suppose that we want to implement the generic version of TimeoutAfter?

It turns out that the implementation of the above would be nearly identical to the non-generic version, except that a TaskCompletionSource<TResult> would be used instead of a TaskCompletionSource<VoidTypeStruct>. The MarshalTaskResults method was already written to correctly handle the marshaling of the results of generic Tasks.

[1] There is no non-generic version of TaskCompletionSource<TResult>. So, if you want a completion source for a Task (as opposed to a Task<TResult>), you still need to provide some throwaway TResult type to TaskCompletionSource. For this example, we’ve created a dummy type (VoidTypeStruct), and we create a TaskCompletionSource<VoidTypeStruct>.

[2] So does it really buy you anything to replace a closure allocation with a tuple allocation? The answer is “yes”. If you were to examine the IL produced from the original code, you would see that both a closure object and a delegate need to be allocated for this call. Eliminating variable capture in the delegate typically allows the compiler to cache the delegate, so in effect two allocations are saved by eliminating variable capture. Thus in this code we’ve traded closure and delegate allocations for a Tuple allocation, so we still come out ahead.

Omer, while that will provide expected behavior for the returned task, it will also block a thread for the duration of the timeout. In contrast, the solutions provided in this post will not block threads unnecessarily. Especially for timer-related functionality, this becomes very important, as it's reasonable to expect a process to have thousands of these active at a time.

After some research I found that this is a .net 4.5 framework feature. Do we know if this can work in a 4.0 framework way. So far my attempts have been unsuccessful. The ContinueWith is never called. Any assistance you can provide would be helpful. Thank you.

@Joe Hoag: I am attempting to use the ContinueWith indicated in the first post with .Net 4. The execution gets to the timers timeout, sets the exception, but then never calls ContinueWith after. I think I am just missing something but not sure what.

@thxmike: The ContinueWith will only be called if the original task (the "task" parameter to TimeoutAfter) completes. When the timer expires, it will cause the task *returned* by TimeoutAfter to complete. So:

Task taskA = Task.Factory.StartNew(…);

taskB = taskA.TimeoutAfter(1000); // 1 second

If the timeout expires, taskB will complete with a TimeoutException. taskA may well continue running.

@Joe Hoag: I get it now. Very Interesting. Thanks for you patience. This next question may be out of scope but what happens to the orphaned taskA, lets say it never stops executing ?(i.e. some run away code, or some type of blocking code which never lets it complete).

@thxmike: The ultimate disposition of taskA is the concern of the person who creates/launches taskA. The purpose of Task.TimeoutAfter is to return a *proxy* (taskB in the example above) for taskA that will either (1) be completed with a TimeoutException if taskA fails to complete within the specified time span, or (2) be completed with taskA's completion information if taskA completes within the specified time span.

This proxy can be used to effectively "time out" a task that was handed to you, which you did not create, without actually perturbing that original task. Consider something like this:

Task taskA; // handed to you

var proxy = taskA.TimeoutAfter(1000); // times out after one second

proxy.ContinueWith( antecedent => {DoSomething(antecedent);});

That continuation can now fire, one way or another, whether or not taskA completes. The DoSomething() method can check to see whether or not "antecedent" completed or timed out, and act accordingly.

@Vamsi: Which example, and what race specifically are you concerned about? In these examples, the timer just serves to complete the task, so the timer's callback won't be doing anything meaningful by the time the continuation is running.

I'm rather new to the concept of multithreading, and was curious how this would be used/called. I've tested the program and as far as I can tell, this will not stop threads from running like a cancellation token pattern would. It appears that it simply provides a task object that you can check the TaskStatus for fault. Am I understanding this correctly, and how is this supposed to be called/used?

@Joey B: You get back a Task object that will complete when either the original task completes or a timeout occurs, and you can then do with that returned Task whatever you like. You can poll for its status as you suggest, but you can also synchronous wait for it to complete (e.g. t.Wait()), you can asynchronously wait for it to complete (e.g. await t;), you can hook up continuations manually (e.g. t.ContinueWith(…)), etc.