All about Async/Await, System.Threading.Tasks, System.Collections.Concurrent, System.Linq, and more…
(The full set of ParallelExtensionsExtras Tour posts is available here.)
In our last ParallelExtensionsExtras tour post, we discussed implementing an extension ToObservable method for Task<TResult>. This is just one of a myriad of extra pieces of functionality that are useful with Tasks, and the TaskExtrasExtensions.cs file in ParallelExtensionsExtras includes several. In this post, we’ll cover a few of the more interesting ones.
When dealing with operations that run asynchronously, it’s not uncommon to want to incorporate a timeout, such that other code that depends on the asynchronous operation isn’t forced to wait too long. We can tackle this use case with a simple WithTimeout method:
public static Task<TResult> WithTimeout<TResult>(
this Task<TResult> task, TimeSpan timeout)
var result = new TaskCompletionSource<TResult>(task.AsyncState);
var timer = new Timer(_ => result.TrySetCanceled(),
null, timeout, TimeSpan.FromMilliseconds(-1));
This method works by creating a TaskCompletionSource to serve as the representation for the potentially timed-out task. A System.Threading.Timer instance is also created to fire once after the user specified period of time. When the timer does fire, it attempts to transition the TaskCompletionSource into a canceled state, which will only take effect if the TaskCompletionSource hasn’t already been completed. In addition, a continuation is used to dispose of the timer and to transition the TaskCompletionSource to a completed state when the original task finishes.
TrySetFromTask is another utility extension method defined in ParallelExtensionsExtras that serves to wrap up some frequently needed logic when using a TaskCompletionSource to serve as a proxy for another Task<TResult>:
public static bool TrySetFromTask<TResult>(
this TaskCompletionSource<TResult> resultSetter, Task<TResult> task)
throw new InvalidOperationException("The task was not completed.");
TrySetFromTask is defined in TaskCompletionSourceExtensions.cs
Tasks implement IAsyncResult, which make them useful in custom implementations of the Asnchronous Programming Model (APM) pattern. However, when implementing the APM pattern, there are some very important rules that need to be followed. For example, the object state parameter passed to the BeginXx method must be returned from the resulting IAsyncResult’s AsyncState property, the AsyncCallback passed to BeginXx must be invoked when the asynchronous operation completes, and it must be invoked with the exact same IAsyncResult returned from the BeginXx method as its argument. We can implement a WithAsyncCallback method to accomplish all of this:
public static Task WithAsyncCallback(
this Task task, AsyncCallback callback, object state)
var tcs = new TaskCompletionSource<object>(state);
if (callback != null) callback(tcs.Task);
As with WithTimeout, a TaskCompletionSource instance is used, here to represent the IAsyncResult that should be both returned from BeginXx and passed to the AsyncCallback. When the source task completes, using ContinueWith we transfer the task’s results to the TaskCompletionSource and we then invoke the AsyncCallback.
Does this timeout start from when the task is created or when the task itself is started? e.g. If I have 100 URLs to download but the task-scheduler can only run 10 tasks at a time will the later tasks already have their timer running which then gives them less time to execute or mean that they could timeout before they ever really get a chance to run?
In the above example of WithTimeout, the timeout is starting when WithTimeout is called. As such, yes, in this particular construction it's possible earlier URLs could be given an advantage over later. That said, in many scenarios this is what you want. If you're trying to ensure that your overall activity doesn't take more than, say, 10 seconds, you actually do want the timer started at the beginning; while it might be more fair to allow each download its full 10 seconds, that could result in the overall activity taking significantly longer than you desire. If you do want that behavior, there are also ways to achieve it.