Creating Higher Level APIs with TPL

The BCL has a bunch of APIs which provide asynchronous versions of operations in the form of either Beginxxx/Endxxx or XyzCompleted, the former known as Asynchronous Programming Model(APM) and the latter known as Event Based Asynchronous Pattern (EAP). TPL provides a nice abstraction for performing asynchronous operations as Tasks using delegates, but what can you do for working with these existing BCL classes which already have an async version?

Here is an example.
Let's say we need to download some data from the web and save it to a file, you would typically use the WebClient class and write something like this today (wrapped in a method of course)
  1. WebClient client = new WebClient();  
  2.            client.DownloadDataAsync(new Uri("http://www.bing.com"));  
  3.            client.DownloadDataCompleted += new DownloadDataCompletedEventHandler((s, e) =>  
  4.            {  
  5.                if (e.Error != null)  
  6.                {  
  7.                    throw e.Error;  
  8.                }  
  9.                if (!e.Cancelled)  
  10.                {  
  11.                    using (FileStream fs = new FileStream("bing.htm", FileMode.CreateNew))  
  12.                    {  
  13.                        fs.Write(e.Result,0,e.Result.Length);  
  14.                        fs.Flush();  
  15.                        fs.Close();  
  16.                    }  
  17.                }  
  18.            }  
  19.            );  
  20.            Console.Read();  

Yuck not very pretty is it! It's kinda smelly and looking at the code, it doesn't really capture the essense of what we are trying to do, besides if the client were to call a method which does this work, we'd have to ensure that the client thread blocks till the continuation completes or conversely provide a mechanism such as a WaitHandle that the client can wait on, not very expressive is it?


Let's see how we can achieve the same results using the Task API.
 
  1. private static TaskCompletionSource<byte[]> CreateSource(object state)  
  2. {  
  3.     return new TaskCompletionSource<byte[]>(state);  
  4. }  
  5.   
  6. private static void SetCompletionSource<T>(  
  7.     TaskCompletionSource<T> tcs,  
  8.     AsyncCompletedEventArgs e,  
  9.     Func<T> getResult)  
  10. {  
  11.     if (e.UserState == tcs)  
  12.     {  
  13.         if (e.Cancelled)  
  14.             tcs.TrySetCanceled();  
  15.         else  
  16.             if (e.Error != null) tcs.TrySetException(e.Error);  
  17.         else  
  18.             tcs.TrySetResult(getResult());  
  19.     }  
  20. }  
  21.   
  22. public static Task<byte[]> DownloadBytesTask(this WebClient webClient, string uri)  
  23. {  
  24.     var source = CreateSource(uri);  
  25.     DownloadDataCompletedEventHandler handler = null;  
  26.     handler = (object s, DownloadDataCompletedEventArgs e) =>  
  27.     {  
  28.         SetCompletionSource(source, e, () => e.Result);  
  29.         webClient.DownloadDataCompleted -= handler;  
  30.     };  
  31.     webClient.DownloadDataAsync(new Uri(uri), source);  
  32.     webClient.DownloadDataCompleted += handler;  
  33.     return source.Task;  
  34. }  

We've wrapped up the async network operation into a Task as an extension method.
Let's dig in a little...

In DownloadBytesTask, we first create a TaskCompletionSource which provides a mechanism to create a Task to hand out to consumers such that the only legal state modification to the Task can be performed via the methods available on the TaskCompletionSource itself and not on the instance of the Task directly, this is precisely the behavior needed for a Task that is downloading content from the web asynchronously;it doesn't make sense for the client to explicitly start such a Task which has already started downloading data and such an operation should really be considered illegal, TaskCompletionSource enables this behavior.

In the callback we call SetCompletionSource which is responsible for transitioning the state of the Task based on the results of the asynchronous operation such as whether it errored out, or completed or whether it was cancelled.

We then proceed to start the asynchronous download by calling DownloadDataAsync method of the WebClient instance.

Couple of things to note:

  • The callback has been unhooked from the event in the callback to ensure that we don't have a memory leak.
  • The Task has already started since "DownloadDataAsync" has already been invoked, and it is this started Task that we return from the method.
The client can now perform the asynchronous download and saving of content like so:
 
  1. public class Program  
  2. {  
  3.         public static Task WriteToFileTask(string fileName, byte[] bytes)  
  4.         {  
  5.             FileStream fs = new FileStream(fileName, FileMode.CreateNew);  
  6.             var task = Task.Factory.FromAsync(fs.BeginWrite, fs.EndWrite, bytes, 0, bytes.Length, null);  
  7.             return task;  
  8.         }  
  9.   
  10.         static void Main(string[] args)  
  11.         {  
  12.             WebClient wc = new WebClient();  
  13.             var task = wc.DownloadBytesTask("http://www.bing.com/");                                     
  14.             var continuationTask = task.ContinueWith(a => WriteToFileTask("bing.htm", a.Result));  
  15.             continuationTask.Wait();              
  16.         }  
  17. }  

The DownloadBytes extension method on the WebClient returns a Task which has already started, we then specify a continuation to execute "after" the Task completes by calling the "ContinueWith" method on the Task instance which in this case is itself a Task which works on the result of the antecedent task (a byte[]) obtained by calling the "WriteToFileTask" method which is responsible for writing out the byte[] to a file.

It's important to note that we have to wait for the "continuation" to complete by calling continuationTask.Wait(), otherwise the process will terminate since we are running on a console app. The same would hold true in case of a Webforms client since the calling Page would continue processing and not wait for the Task to complete.

Notice how the client code seems pretty terse in terms of expressing the intent. One can easily read the code and figure out what's going on.
As always happy coding!

Print | posted @ Sunday, August 23, 2009 3:46 PM

Comments on this entry:

No comments posted yet.

Post A Comment
Title:
Name:
Email:
Comment:
Verification: