Asynchronous I/O in C#: Why tasks (a.k.a. promises, futures)?

In previous posts I provided an introduction to asynchronous I/O in C#, and also dug a bit deeper into I/O completion ports, which is one of the possible mechanisms that the .NET frameworks uses to accomplish this.

In this blog post we are going to go over the benefits of using the Task abstraction to represent asynchronous operations. Before I get started, I recommend that if you have never watched this video you should go ahead and do it. Michael Jackson (@mjackson) and Domenic Denicola (@domenic) do a great job explaining the benefits of promises.

What are tasks?

A Task is an object oriented abstraction that represents an asynchronous operation. It does not carry any special connotation as to how that operation is being performed (see my previous post). The concept of task is similar to the concept of promise or future and, as a matter of fact, in the Parallel Extensions CTP the class' original name was Future.

That being said, all of the operations for which the .NET framework now returns a Task used to always be available but were somewhat harder to use. Let's look at a couple of examples so I can better express what I mean.

A walk through history lane (with examples)

In the following examples, let's assume that our goal is to download HTML from a couple of URLs and once both downloads are done we need to do something with the HTML. What we are going to do is compare how the different proposals for asynchronous programming in .NET allow you to do this.

The source code for the samples can be found at GitHub. I have tried to make all samples look similar, which leads to a bit of code duplication, but I think that makes the comparisons simpler. Basically, all samples follow this model:

Asynchronous Programming Model (APM)

The first model that the Framework proposed for asynchronous programming was the asynchronous programming model.

As you can see in the code below, we need to add an extra variable to keep track of the amount of pending requests. Additionally, we need to add a lock object to avoid a race condition for downloads that complete at the same time (if you want to try it out just remove the lock and add a Thread.Sleep(10000) after decrementing the pending count). Imagine the work that you would need to do to add error handling…

Event-based asynchronous pattern (EAP)

After APM came the event-based asynchronous pattern. It's similar to the APM example, but instead of using callbacks and IAsyncResult it leverages events. An interesting addition to this model is the possibility of cancelling a particular asynchronous operation (not shown here, it is just part of the spec).

Task-based asynchronous pattern (TPM)

Finally, we get to the task-based asynchronous pattern. The relevant parts here are:

  • The usage of the Task.WhenAll, which greatly reduces the amount of code required to get this working, but most importantly makes the intent clear.
  • The usage of ContinueWith, which allows you to provide code that does some work when a task finishes and returns a new task that will be completed when the work is done.

Note: I’m not using async/await as I want to focus on Tasks. In a future post, the relationship between tasks and async/await will be covered.

Abstracting APM and EAP with Tasks

As I stated at the beginning of the blog post, Tasks represent an abstraction for an asynchronous operation. For that reason, all other programming models can be abstracted using Tasks. I have created a couple of extension methods that do this exact thing. These methods are just meant as an example. If you are going to be doing this for real you should first make sure that the methods are not already implemented, and if that is that case use the TaskFactory class.

With those extensions the APM example is greatly simplified (I won’t show the updated EAP sample, as it looks similar).

A final example: Cancellation

What if we wanted to only carry out work with only the first download that finishes (e.g.: useful when working against multiple CDNs) and also avoid performing unnecessary downloads? Then we have to find a way to execute code once the first Task completes and cancel the other one. The following code does just that (I'm not even going to bother doing this with the other models, it requires a lot more thinking). The key takeaways from the following code are:

  • The usage of the Task.WhenAny to execute the continuation when the task that finishes sooner completes.
  • Providing TaskContinuationOptions for the continuation.
  • Providing a cancellation Token for the download tasks and cancelling when the task that finishes sooner completes.