The Nature of TaskCompletionSource<TResult>

The Nature of TaskCompletionSource<TResult>

Rate This
  • Comments 11

The Task Parallel Library is centered around the Task class and its derived Task<TResult>. The main purpose of these types is to represent the execution of an asynchronous workload and to provide an object with a means to operate on that workload, whether it be to wait for it, to continue from it, or the like. The primary type of asynchronous workload supported by Task is the execution of a delegate, either an Action or a Func<T>, such that the delegate’s execution in the underlying scheduler is represented by the Task. But in any compositional system that wants to use Task as its centerpiece, just support for asynchronous delegate execution isn’t enough: support must be provided for other asynchronous operations as well. For example, there are a variety of asynchronous operations already implemented in the .NET Framework and exposed through the Asynchronous Programming Model (APM) pattern or the Event-Based Asynchronous Pattern (EAP). In both of these cases, we’d like to be able to refer to these asynchronous operations as Tasks and operate on them as Tasks, even though the underlying work isn’t necessarily being performed by scheduling and executing a delegate. (More to come on both of those in future posts.)

To support such a paradigm with Tasks, we need a way to retain the Task façade and the ability to refer to an arbitrary asynchronous operation as a Task, but to control the lifetime of that Task according to the rules of the underlying infrastructure that’s providing the asynchrony, and to do so in a manner that doesn’t cost significantly. This is the purpose of TaskCompletionSource<TResult>.

The TaskCompletionSource<TResult> type serves two related purposes, both alluded to by its name: it is a source for creating a task, and the source for that task’s completion. In essence, a TaskCompletionSource<TResult> acts as the producer for a Task<TResult> and its completion. You create a TaskCompletionSource<TResult> and hand the underlying Task<TResult> it’s created, accessible from its Task property. Unlike Tasks created by Task.Factory.StartNew, the Task handed out by TaskCompletionSource<TResult> does not have any scheduled delegate associated with it. Rather, TaskCompletionSource<TResult> provides methods that allow you as the developer to control the lifetime and completion of the associated Task. This includes SetResult, SetException, and SetCanceled, as well as TrySet* variants of each of those. (A Task may only be completed once, thus attempting to set a Task into a completed state when it’s already in a completed state is an error, and the Set* methods will throw. However, as we’re dealing with concurrency here, and there are some situations where races may be expected between multiple threads trying to resolve the completion source, the TrySet* variants return Booleans indicating success rather than throwing an exceptions.)

As a simple example, imagine for a moment that you didn’t have Task.Factory.StartNew, and thus you needed a way to execute a Func<T> asynchronously and have a Task<T> to represent that operation. This could be done with a TaskCompletionSource<T> as follows:

public static Task<T> RunAsync<T>(Func<T> function)
{
    if (function == null) throw new ArgumentNullException(“function”);
    var tcs = new TaskCompletionSource<T>();
    ThreadPool.QueueUserWorkItem(_ =>
    {
        try
        { 
            T result = function();
            tcs.SetResult(result); 
        }
        catch(Exception exc) { tcs.SetException(exc); }
    });
    return tcs.Task;
}

The operation is being performed asynchronously through a mechanism unknown to the TaskCompletionSource<T>. All it knows is that at some point, its SetResult or SetException method is being called to complete the Task<T> exposed through its Task property.

Note, too, that because Task<TResult> derives from Task, we can use the generic TaskCompletionSource<TResult> under the covers for methods that work in terms of Task rather than in terms of Task<TResult>. For example, consider the same RunAsync method just shown, but accepting an Action (which returns void) rather than a Func<T> (which returns T).

public static Task RunAsync(Action action)
{
    var tcs = new TaskCompletionSource<Object>();
    ThreadPool.QueueUserWorkItem(_ =>
    {
        try
        {
            action();
            tcs.SetResult(null);
        }
        catch(Exception exc) { tcs.SetException(exc); }
    });
    return tcs.Task;
}

Since we no longer care what the type of T is, I’ve defaulted to using Object. Then, when the Action is executed successfully, SetResult is still used to transition the Task into the RanToCompletion final state; however, since the actual result value is irrelevant, null is used. Finally, RunAsync returns Task rather than Task<Object>. Of course, the instantiated task’s type is still Task<Object>, but we need not refer to it as such, and the consumer of this method need not care about those implementation details.

In future posts, we’ll look at how TaskCompletionSource<TResult> is a staple in a developer’s toolbox, including the developers of the Task Parallel Library itself, where TaskCompletionSource<TResult> is used liberally internally.

Leave a Comment
  • Please add 6 and 5 and type the answer here:
  • Post
Page 1 of 1 (11 items)