Exposing .NET tasks as WinRT asynchronous operations
In the blog post Diving Deep with Await and WinRT, we discussed the new async and await keywords in C# and Visual Basic and how you can use them to consume Windows Runtime (WinRT) asynchronous operations.
With some assistance from the .NET Base Class Libraries (BCL), you can also use these keywords to develop asynchronous operations that are then exposed via WinRT for other components built in other languages to consume. In this post, we’ll explore how to do so. (For overall details on implementing WinRT components with C# or Visual Basic, see Creating Windows Runtime Components in C# and Visual Basic.)
Let’s start by reviewing the shape of asynchronous APIs in WinRT.
WinRT async interfaces
WinRT has several interfaces related to asynchronous operations. The first is IAsyncInfo, which every valid WinRT async operation implements. It surfaces the common capabilities for an asynchronous operation, including the operation’s current status, the operation’s error if it failed, an ID for the operation, and the operation’s ability to have cancellation requested:
public interface IAsyncInfo { AsyncStatus Status { get; } Exception ErrorCode { get; } uint Id { get; } void Cancel(); void Close(); }
In addition to implementing IAsyncInfo, every valid WinRT asynchronous operation implements one of four additional interfaces: IAsyncAction, IAsyncActionWithProgress<TProgress> , IAsyncOperation<TResult> , or IAsyncOperationWithProgress<TResult,TProgress> . These additional interfaces allow the consumer to set a callback that will be invoked when the asynchronous work completes, and optionally allow for the consumer to get a result and/or to receive progress reports:
// No results, no progress public interface IAsyncAction : IAsyncInfo { AsyncActionCompletedHandler Completed { get; set; } void GetResults(); } // No results, with progress public interface IAsyncActionWithProgress<TProgress> : IAsyncInfo { AsyncActionWithProgressCompletedHandler<TProgress> Completed { get; set; } AsyncActionProgressHandler<TProgress> Progress { get; set; } void GetResults(); } // With results, no progress public interface IAsyncOperation<TResult> : IAsyncInfo { AsyncOperationCompletedHandler<TResult> Completed { get; set; } TResult GetResults(); } // With results, with progress public interface IAsyncOperationWithProgress<TResult,TProgress> : IAsyncInfo { AsyncOperationWithProgressCompletedHandler<TResult,TProgress> Completed { get; set; } AsyncOperationProgressHandler<TResult,TProgress> Progress { get; set; } TResult GetResults(); }
(IAsyncAction provides a GetResults method even though there are no results to be returned. This provides a method on a faulted async operation to throw its exception, rather than forcing all consumers to access the IAsyncInfo’s ErrorCode property. It is similar to how awaiter types in C# and Visual Basic expose a GetResult method, even if that GetResult method is typed to return void.)
When building a WinRT library, all publicly exposed asynchronous operations in that library are strongly typed to return one of these four interfaces. In contrast, new asynchronous operations exposed from .NET libraries follow the Task-based Asynchronous Pattern (TAP), returning Task or Task<TResult> , the former for operations that don’t return a result, and the latter for operations that do.
Task and Task<TResult> don’t implement these WinRT interfaces, nor does the Common Language Runtime (CLR) implicitly paper over the differences (as it does do for some types, such as the WinRT Windows.Foundation.Uri type and the BCL System.Uri type). Instead, we need to explicitly convert from one world to the other. In the Diving Deep with Await and WinRT post, we saw how the AsTask extension method in the BCL provides an explicit cast-like mechanism to convert from the WinRT async interfaces to .NET Tasks. The BCL also supports the other direction, with methods to convert from .NET Tasks to the WinRT async interfaces.
Converting with AsAsyncAction and AsAsyncOperation
For the purposes of this blog post, let’s assume we have a .NET asynchronous method DownloadStringAsyncInternal. We pass to it a URL to a web page, and the method asynchronously downloads and returns the contents of that page as a string:
internal static Task<string> DownloadStringAsyncInternal(string url);
How this method is implemented doesn’t matter. Rather, our goal is to wrap this as a WinRT asynchronous operation, meaning as a method that returns one of the four previously mentioned interfaces. As our operation has a result (a string) and as it doesn’t support progress reporting, our WinRT async operation returns IAsyncOperation<string> :
public static IAsyncOperation<string> DownloadStringAsync(string url);
To implement this method, we can call the DownloadStringAsyncInternal method to get the resulting Task<string> . Then we need to convert that task to the required IAsyncOperation<string> … but how?
public static IAsyncOperation<string> DownloadStringAsync(string url) { Task<string> from = DownloadStringAsyncInternal(url); IAsyncOperation<string> to = ...; // TODO: how do we convert 'from'? return to; }
To address this gap, the System.Runtime.WindowsRuntime.dll assembly in .NET 4.5 includes extension methods for Task and Task<TResult> that provide the necessary conversions:
// in System.Runtime.WindowsRuntime.dll public static class WindowsRuntimeSystemExtensions { public static IAsyncAction AsAsyncAction( this Task source); public static IAsyncOperation<TResult> AsAsyncOperation<TResult>( this Task<TResult> source); ... }
These methods return a new IAsyncAction or IAsyncOperation<TResult> instance that wraps the supplied Task or Task<TResult> , respectively (because Task<TResult> derives from Task, both of these methods are available for Task<TResult> , though it’s relatively rare that you would use AsAsyncAction with a result-returning asynchronous method). Logically, you can think of these operations as explicit, synchronous casts, or from a design pattern perspective, as adapters. They return an instance that represents the underlying task but that exposes the required surface area for WinRT. With such extension methods, we can complete our DownloadStringAsync implementation:
public static IAsyncOperation<string> DownloadStringAsync(string url) { Task<string> from = DownloadStringAsyncInternal(url); IAsyncOperation<string> to = from.AsAsyncOperation(); return to; }
We can also write this more succinctly, highlighting how very cast-like the operation is:
public static IAsyncOperation<string> DownloadStringAsync(string url) { return DownloadStringAsyncInternal(url).AsAsyncOperation(); }
DownloadStringAsyncInternal is being invoked before we ever call AsAsyncOperation. This means that we need the synchronous call to DownloadStringAsyncInternal to return quickly to ensure that the DownloadStringAsync wrapper method is responsive. If for some reason you fear the synchronous work you’re doing will take too long, or if you explicitly want to offload the invocation to a thread pool for other reasons, you can do so using Task.Run, then invoking AsAsyncOperation on its returned task:
public static IAsyncOperation<string> DownloadStringAsync(string url) { return Task.Run(()=>DownloadStringAsyncInternal(url)).AsAsyncOperation(); }
More flexibility with AsyncInfo.Run
These built-in AsAsyncAction and AsAsyncOperation extension methods are great for simple conversions from Task to IAsyncAction and from Task<TResult> to IAsyncOperation<TResult> . But what about more advanced conversions?
System.Runtime.WindowsRuntime.dll contains another type that provides more flexibility: AsyncInfo, in the System.Runtime.InteropServices.WindowsRuntime namespace. AsyncInfo exposes four overloads of a static Run method, one for each of the four WinRT async interfaces:
// in System.Runtime.WindowsRuntime.dll public static class AsyncInfo { // No results, no progress public static IAsyncAction Run( Func<CancellationToken, Task> taskProvider); // No results, with progress public static IAsyncActionWithProgress<TProgress> Run<TProgress>( Func<CancellationToken, IProgress<TProgress>, Task> taskProvider); // With results, no progress public static IAsyncOperation<TResult> Run<TResult>( Func<CancellationToken, Task<TResult>> taskProvider); // With results, with progress public static IAsyncOperationWithProgress<TResult, TProgress> Run<TResult, TProgress>( Func<CancellationToken, IProgress<TProgress>, Task<TResult>> taskProvider); }
The AsAsyncAction and AsAsyncOperation methods we’ve already examined accept a Task as an argument. In contrast, these Run methods accept a function delegate that returns a Task, and this difference between Task and Func<…,Task> is enough to give us the added flexibility we need for more advanced operations.
Logically, you can think of AsAsyncAction and AsAsyncOperation as being simple helpers on top of the more advanced AsyncInfo.Run:
public static IAsyncAction AsAsyncAction( this Task source) { return AsyncInfo.Run(_ => source); } public static IAsyncOperation<TResult> AsAsyncOperation<TResult>( this Task<TResult> source) { return AsyncInfo.Run(_ => source); }
This isn’t exactly how they’re implemented in .NET 4.5, but functionally they behave this way, so it helps to think about them as such to contrast between the basic and advanced support. If you have a simple case, use AsAsyncAction and AsAsyncOperation, but there are several advanced cases where AsyncInfo.Run shines.
Cancellation
AsyncInfo.Run makes it possible to support cancellation with WinRT async methods.
To continue with our downloading example, let’s say we have another DownloadStringAsyncInternal overload that accepts a CancellationToken:
internal static Task<string> DownloadStringAsyncInternal( string url, CancellationToken cancellationToken);
CancellationToken is .NET Framework type that supports cooperative cancellation in a composable manner. You can pass a single token into any number of method calls, and when that token has cancellation requested (via the CancellationTokenSource that created the token), the cancellation request is then visible to all of those consuming operations. This approach differs slightly from that used by WinRT async, which is to have each individual IAsyncInfo expose its own Cancel method. Given that, how do we arrange for a call to Cancel on the IAsyncOperation<string> to have cancellation requested on a CancellationToken that’s passed into DownloadStringAsyncInternal? AsAsyncOperation won’t work in this case:
public static IAsyncOperation<string> DownloadStringAsync(string uri) { return DownloadStringAsyncInternal(uri, … /* what goes here?? */) .AsAsyncOperation(); }
To know when cancellation is requested of the IAsyncOperation<string> , that instance would need to somehow notify a listener that its Cancel method was called, for example by requesting cancellation of a CancellationToken that we’d pass into DownloadStringAsyncInternal. But in a classic “catch-22,” we don’t get the Task<string> on which to invoke AsAsyncOperation until we’ve already invoked DownloadStringAsyncInternal, at which point we would have already needed to supply the very CancellationToken we’d have wanted AsAsyncOperation to provide.
There are multiple ways to solve this conundrum, including the solution employed by AsyncInfo.Run. The Run method is responsible for constructing the IAsyncOperation<string> , and it creates that instance to request cancellation of a CancellationToken that it also creates when the async operation’s Cancel method is called. Then when invoking the user-supplied delegate passed to Run, it passes in this token, avoiding the previously discussed cycle:
public static IAsyncOperation<string> DownloadStringAsync(string uri) { return AsyncInfo.Run(cancellationToken => DownloadStringAsyncInternal(uri, cancellationToken)); }
Lambdas and Anonymous Methods
AsyncInfo.Run simplifies using lambda functions and anonymous methods to implement WinRT async methods.
For example, if we didn’t already have the DownloadStringAsyncInternal method, we might implement it and DownloadStringAsync like this:
public static IAsyncOperation<string> DownloadStringAsync(string uri) { return AsyncInfo.Run(delegate(CancellationToken cancellationToken) { return DownloadStringAsyncInternal(uri, cancellationToken)); }); } private static async Task<string> DownloadStringAsyncInternal( string uri, CancellationToken cancellationToken) { var response = await new HttpClient().GetAsync( uri, cancellationToken); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); }
By taking advantage of the C# and Visual Basic support for writing asynchronous anonymous methods, we can simplify our implementation by combining these two methods into one:
public static IAsyncOperation<string> DownloadStringAsync(string uri) { return AsyncInfo.Run(async delegate(CancellationToken cancellationToken) { var response = await new HttpClient().GetAsync( uri, cancellationToken); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); }); }
Progress
AsyncInfo.Run also provides support for progress reporting through WinRT async methods.
Instead of our DownloadStringAsync method returning an IAsyncOperation<string> , imagine if we instead wanted it to return an IAsyncOperationWithProgress<string,int> :
public static IAsyncOperationWithProgress<string,int> DownloadStringAsync(string uri);
DownloadStringAsync now can provide progress updates containing integral data, such that consumers can set a delegate as the interface’s Progress property to receive notifications of progress change.
AsyncInfo.Run provides an overload that accepts a Func<CancellationToken,IProgress<TProgress>,Task<TResult>> . Just as AsyncInfo.Run passes into the delegate a CancellationToken that will have cancellation requested when the Cancel method is called, it can also pass in an IProgress<TProgress> instance whose Report method triggers invocations of the consumer’s Progress delegate. For example, if we wanted to modify our previous example to report 0% progress at the beginning, 50% progress after getting the response back, and 100% progress after parsing the response into a string, that might look like this:
public static IAsyncOperationWithProgress<string,int> DownloadStringAsync(string uri) { return AsyncInfo.Run(async delegate( CancellationToken cancellationToken, IProgress<int> progress) { progress.Report(0); try { var response = await new HttpClient().GetAsync(uri, cancellationToken); progress.Report(50); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } finally { progress.Report(100); } }); }
A look under the cover
To get a good mental model for how all of this works, let’s explore an implementation of AsAsyncOperation and AsyncInfo.Run. These are not the same implementations that exist in .NET 4.5, and they aren’t as robust. Rather, they are approximations that provide a good sense of how things work under the cover, and are not intended to be used in production.
AsAsyncOperation
The AsAsyncOperation method takes a Task<TResult> and returns an IAsyncOperation<TResult> :
public static IAsyncOperation<TResult> AsAsyncOperation<TResult>( this Task<TResult> source);
To implement this method, we need to create a type that implements IAsyncOperation<TResult> and that wraps the supplied task.
public static IAsyncOperation<TResult> AsAsyncOperation<TResult>( this Task<TResult> source) { return new TaskAsAsyncOperationAdapter<TResult>(source); } internal class TaskAsAsyncOperationAdapter<TResult> : IAsyncOperation<TResult> { private readonly Task<TResult> m_task; public TaskAsAsyncOperationAdapter(Task<TResult> task) { m_task = task; } ... }
Each of the interface method implementations on this type will delegate to functionality on the wrapped task. Let’s start with the easiest members.
First, the IAsyncInfo.Close method is supposed to aggressively clean up any resources that the completed asynchronous operation used. As we have no such resources (our object just wraps a task), our implementation is empty:
public void Close() { /* NOP */ }
With the IAsyncInfo.Cancel method a consumer of the async operation can request its cancellation. It is purely a request, and doesn’t in any way force the operation to exit. We simply use a CancellationTokenSource to store that a request occurred:
private readonly CancellationTokenSource m_canceler = new CancellationTokenSource(); public void Cancel() { m_canceler.Cancel(); }
The IAsyncInfo.Status property returns an AsyncStatus to represent the current status of the operation with regards to its asynchronous lifecycle. This can be one of four values: Started, Completed, Error, or Canceled. For the most part, we can simply delegate to the underlying Task’s Status property and map from its returned TaskStatus to the needed AsyncStatus:
From TaskStatus |
To AsyncStatus |
RanToCompletion |
Completed |
Faulted |
Error |
Canceled |
Canceled |
All other values & cancellation was requested |
Canceled |
All other values & cancellation was not requested |
Started |
If the Task is not yet completed and cancellation has been requested, we need to return Canceled instead of Started. This means that although TaskStatus.Canceled is only a terminal state, AsyncStatus.Canceled can be either a terminal state or a non-terminal state, in that it’s possible for an IAsyncInfo to end in the Canceled state, or for an IAsyncInfo in the Canceled state to transition to either AsyncStatus.Completed or AsyncStatus.Error.
public AsyncStatus Status { get { switch (m_task.Status) { case TaskStatus.RanToCompletion: return AsyncStatus.Completed; case TaskStatus.Faulted: return AsyncStatus.Error; case TaskStatus.Canceled: return AsyncStatus.Canceled; default: return m_canceler.IsCancellationRequested ? AsyncStatus.Canceled : AsyncStatus.Started; } } }
The IAsyncInfo.Id property returns a UInt32 identifier for the operation. As Task itself already exposes such an identifier (as an Int32), we can implement this property simply by delegating through to the underlying Task property:
public uint Id { get { return (uint)m_task.Id; } }
WinRT defines the IAsyncInfo.ErrorCode property to return an HResult. But the CLR internally maps the WinRT HResult to a .NET Exception, surfacing it that way to us through the managed projection. Task itself exposes an Exception property, so we can just delegate through to it: if the Task ended in the Faulted state, we return its first exception (a Task could potentially fault due to multiple exceptions, such as with a Task returned from Task.WhenAll), returning null otherwise:
public Exception ErrorCode { get { return m_task.IsFaulted ? m_task.Exception.InnerException : null; } }
That’s it for implementing IAsyncInfo. Now we need to implement the two additional members supplied by IAsyncOperation<TResult> : GetResults and Completed.
Consumers call GetResults after the operation completes successfully or after an exception occurs. In the former case, it returns the computed result, and in the latter case, it throws the relevant exception. If the operation ended as Canceled, or if it hasn’t ended yet, it’s illegal to invoke GetResults. As such, we can implement GetResults as follows, relying on the task’s awaiter’s GetResult method to return the result if successful or to propagate the right exception if the task ended as Faulted.
public TResult GetResults() { switch (m_task.Status) { case TaskStatus.RanToCompletion: case TaskStatus.Faulted: return m_task.GetAwaiter().GetResult(); default: throw new InvalidOperationException("Invalid GetResults call."); } }
Finally, we have the Completed property. Completed represents a delegate that should be invoked when the operation completes. If the operation has already finished when Completed is set, the supplied delegate must be invoked or scheduled immediately. Additionally, a consumer can set the property only once (attempts to set multiple times result in exceptions). And after the operation has been completed, implementations must drop the reference to the delegate to avoid memory leaks. We can rely on Task’s ContinueWith method to implement much of this behavior, but because ContinueWith can be used multiple times on the same Task instance, we need to manually implement the more restrictive “set once” behavior:
private AsyncOperationCompletedHandler<TResult> m_handler; private int m_handlerSet; public AsyncOperationCompletedHandler<TResult> Completed { get { return m_handler; } set { if (value == null) throw new ArgumentNullException("value"); if (Interlocked.CompareExchange(ref m_handlerSet, 1, 0) != 0) throw new InvalidOperationException("Handler already set."); m_handler = value; var sc = SynchronizationContext.Current; m_task.ContinueWith(delegate { var handler = m_handler; m_handler = null; if (sc == null) handler(this, this.Status); else sc.Post(delegate { handler(this, this.Status); }, null); }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } }
With that, our implementation of AsAsyncOperation is complete, and we can use any Task<TResult> -returning method as an IAsyncOperation<TResult> method.
AsyncInfo.Run
Now, what about the more advanced AsyncInfo.Run?
public static IAsyncOperation<TResult> Run<TResult>( Func<CancellationToken,Task<TResult>> taskProvider);
To support this Run overload, we can use the same TaskAsAsyncOperationAdapter<TResult> type we just created, with all of our existing implementation intact. In fact, we just need to augment it with the ability to work in terms of Func<CancellationToken,Task<TResult>> instead of only in terms of Task<TResult> . When such a delegate is provided, we can simply invoke it synchronously, passing in the m_canceler CancellationTokenSource we defined earlier and storing the returned task:
internal class TaskAsAsyncOperationAdapter<TResult> : IAsyncOperation<TResult> { private readonly Task<TResult> m_task; public TaskAsAsyncOperationAdapter(Task<TResult> task) { m_task = task; } public TaskAsAsyncOperationAdapter( Func<CancellationToken,Task<TResult>> func) { m_task = func(m_canceler.Token); } ... } public static class AsyncInfo { public static IAsyncOperation<TResult> Run<TResult>( Func<CancellationToken, Task<TResult>> taskProvider) { return new TaskAsAsyncOperationAdapter<TResult>(taskProvider); } … }
This implementation highlights that there’s no magic happening here. AsAsyncAction, AsAsyncOperation, and AsyncInfo.Run are all just helper implementations that save you from having to write all of this boilerplate yourself.
Conclusion
At this point, I hope you have a good understanding of what AsAsyncAction, AsAsyncOperation, and AsyncInfo.Run do for you: they make it easy to take a Task or a Task<TResult> , and expose it as an IAsyncAction, an IAsyncOperation<TResult> , an IAsyncActionWithProgress<TProgress> , or an IAsyncOperationWithProgress<TResult,TProgress> . Combined with the async and await keywords in C# and Visual Basic, this makes it very easy to implement new WinRT asynchronous operations in managed code.
As long as the functionality you’re exposing is available as a Task or Task<TResult> , you should rely on these built-in capabilities to do the conversions for you instead of implementing the WInRT async interfaces manually. And if the functionality you’re trying to expose is not available yet as a Task or Task<TResult> , try to expose it as a Task or Task<TResult> first, and then rely on the built-in conversions. It can be quite difficult to get all of the semantics around a WinRT asynchronous interface implementation correct, which is why these conversions exist to do it for you. Similar support also exists if you’re implementing WinRT asynchronous operations in C++, whether through the base AsyncBase class in the Windows Runtime Library, or through the create_async function in the Parallel Pattern Library.
Happy async’ing.
--Stephen Toub, Visual Studio
Comments
Anonymous
June 17, 2012
Great article, thanks. You use Task.Run in one example to help with responsiveness. How should I think about this compared to ThreadPool.RunAsync. Are they basically the same?Anonymous
June 18, 2012
Hi Andy- Both Task.Run (part of the BCL) and ThreadPool.RunAsync (part of WinRT) support offloading work to a pool of threads so as to avoid blocking the current thread while the specified computation executes. There are some programming model differences between the two, of course. For example, Task.Run provides multiple overloads that work with different delegate types: Action, Func<TResult>, Func<Task>, and Func<Task<TResult>. The overload that accepts an Action is very similar to ThreadPool.RunAsync, in that the delegate represents a synchronous chunk of work that doesn't return a value; with this overload, Task.Run returns a Task (ThreadPool.RunAsync returns an IAsyncAction). The Task.Run overload that accepts a Func<TResult> also represents a synchronous chunk of work, but it does return a value, of type TResult; when used, Task.Run will return a Task<TResult>, such that the returned task object surfaces the result value eventually returned by the delegate's invocation. The other two overloads that accept Func<Task> and Func<Task<TResult>> support the special (but common) case where the provided delegate does asynchronous work. If these overloads didn't exist, then if you provided a Func<Task<TResult>> delegate to Task.Run, Task.Run would return a Task<Task<TResult>>, where instead what you almost always want is a Task<TResult>; in other words, you want Task.Run to return a task that will complete when the task returned from the delegate completes, not at the moment when the delegate itself completes and returns the task. You can see this in the example used in the post: you want the task returned from Task.Run to complete when the task returned from DownloadStringAsyncInternal completes, not when the call to DownloadStringAsyncInternal synchronously returns its task (which may not yet be completed). For more information, see the related blog posts at blogs.msdn.com/.../10265476.aspx and blogs.msdn.com/.../10229468.aspx. I hope that helps.Anonymous
June 27, 2012
Nice post Stephen... I guess the least blogged section of WinRT is System.Threading and System.Threading.Tasks.... I was also recently blabbing about httpclient and IAsyncOperationWithProgress with chunk-ed streams. ( canbilgin.wordpress.com/.../download-send-request-asyncwithprogress-with-httpclient )Anonymous
June 27, 2012
Thanks, Can. I'm glad you enjoyed the post.