Improving Your Asynchronous Code Using Tasks, Async and Await

| Posted by Dave Marini 0 Followers on May 06, 2014. Estimated reading time: 16 minutes |

A note to our readers: As per your request we have developed a set of features that allow you to reduce the noise, while not losing sight of anything that is important. Get email and web notifications by choosing the topics you are interested in.

Asynchronous programming in .NET applications has long been a useful way to perform operations that don’t necessarily need to hold up the flow or responsiveness of an application. Generally, these are either compute-bound operations or I/O bound operations. Compute-bound operations are those where computations can be done on a separate thread, leaving the main thread to continue its own processing, while I/O bound operations involve work that takes place externally and may not need to block a thread while such work takes place. Common examples of I/O bound operations are file and database read/write interactions, as well as network operations. In this article, we’ll examine the Asynchronous Programming Model that served as the standard until the release of .NET 4. Then we’ll look at the Task-based Asynchronous Programming model introduced with .NET 4 and see how, combined with the async and await modifiers introduced in C# 5, it can make asynchronous programming simpler to develop, understand, and maintain.

Traditional Async Using the Asynchronous Programming Model (APM)

Prior to the .NET 4 release, two paradigms existed for implementing asynchronous operations in applications. The Event-based Asynchronous Model (EAM) which employs a combination of methods and event handlers to model the asynchronous operation, and the Asynchronous Programming Model (APM), characterized by Begin and End methods demarking the start and finish of an asynchronous operation and an object structure (IAsyncResult) that represents the state of the operation. Of the two patterns, the APM model was recommended for most scenarios and the framework has widespread support built in for using this model. Let’s take a look at a standard APM implementation of reading a file asynchronously:

static void Main(string[] args)        {            byte[] readBuffer;            var fs = File.OpenRead(@"c:\somefile.txt");            readBuffer = new byte[fs.Length];            var result = fs.BeginRead(readBuffer, 0, (int)fs.Length, OnReadComplete, fs);

//do other work here while file is read...
}
private static void OnReadComplete(IAsyncResult result)
{
var stream = (FileStream)result.AsyncState;
stream.Dispose();
}

Some of the hardships of the APM model are immediately apparent. Firstly, the EndRead method must be called on the stream object which, unless made global, needs to be passed into the state parameter of the BeginRead method so we can retrieve it from within the OnReadComplete callback. Secondly, the fact that we are using a callback means we can’t wrap our filestream in a using block and must explicitly remember to dispose of the filestream in the callback. Finally, the use of the APM pattern for multiple asynchronous operations in a larger codebase leads to readability and maintainability issues as it becomes more and more difficult to associate callbacks for similar operations to their specific beginning statements. To alleviate some of these shortcomings, many developers use in-line lambdas with APM. A refactoring of our example is below:

            byte[] readBuffer;
var fs = File.OpenRead(@"c:\somefile.txt”);
readBuffer = new byte[fs.Length];
var result = fs.BeginRead(readBuffer, 0, (int)fs.Length, asyncResult =>
{
fs.Dispose();
}, null);
//do other work here while file is read...
Console.ReadLine();

In this example, we remove the need to pass the FileStream instance into the callback since the lambda will access it via the closure. Our readability has improved a bit because the callback can be easily related to the original BeginRead call it belongs to. While this makes things somewhat better, there will still be readability issues if the callback were more complex or had one or more subsequent async operations that needed to be coordinated. Some other shortcomings of the APM model are:

• No inherent support for cancellation – From the call to Begin until the callback fires there is no way to cancel what’s happening in the background. In our example, if my file was a gigabyte in length, I wouldn’t be able to stop the read once BeginRead was called. Obviously it would be possible to break down the async read into chunks which can make use of a CancellationTokenSource to stop processing in the middle of the file, but this adds complexity and additional code.
• Callbacks are not synchronized to caller thread – In the APM model, callbacks take place on thread pool threads, which means a callback that needs to interact with UI elements has to check the CompletedSynchronously property of the IAsyncResult object any may need to include code to marshal to the UI thread.
• Coordination with multiple asynchronous operations is difficult – In our example above, if we wanted to have the main thread wait until two distinct files were read into memory, we would need to use thread synchronization objects like waithandles to control the flow of the main thread from the callbacks.

Introduction of Task-Based Asynchronous Programming (TAP)

With the release of .NET 4 came a new pattern called the Task-Based Asynchronous Programming (TAP) model. The basic concept of the new pattern was to represent asynchronous operations in a single method and combine both the status of the operation and an API for interacting with these operations into a single object. This object is the Task and Task<T> classes that are part of the System.Threading.Tasks namespace. The TAP model is now the recommended approach for asynchronous programming, and since .NET 4.5 many classes with support for APM now also have Async methods that return Task or Task<T>. Let’s see how we can read a file asynchronously using the TAP model:

static void Main(string[] args)
{
var fs = File.OpenRead(@"c:\somefile.txt”);
var readBuffer = new byte[fs.Length];
{

else
Console.WriteLine("Exception occurred");

fs.Dispose();
});

//do other work here while file is read...

}

The ReadAsync method of the FileStream class returns a Task<int>. That means the asynchronous operation will eventually contain an int result, which in this case will indicate the number of bytes read from the file. Unlike with the APM model, the returned Task<int> object can be passed around and a continuation can be declared for it at any point during or even after the completion of the file read. The ContinueWith extension method allows us to specify an Action<Task<T>> that will be run when the asynchronous operation completes. If ContinueWith is called after the task is completed, the delegate will be run immediately in a synchronous fashion. Want to read two files asynchronously and then proceed only when both of them have completed? This is simple thanks to the WhenAll extension method on the Task object:

static void Main(string[] args)
{

.ContinueWith(task => Console.WriteLine("All files have been read successfully."));

//do other work here while files are read...
}
{
var fs = File.OpenRead(filePath);
var readBuffer = new byte[fs.Length];
Console.WriteLine("Read {0} bytes successfully from file {1}", task.Result, filePath);
else
Console.WriteLine("Exception occurred while reading file {0}.", filePath);

fs.Dispose();
});

}

Let’s review the APM main points the TAP model addresses:

• Cancellation Support – The Task class supports cancellation from the ground up. As a result, most of framework methods that support the TAP model will have an overload that takes in a CancellationToken. This is true of the FileStream.ReadAsync method, which makes adding cancellation support for large files much easier than coordinating a chunked asynchronous read.
• Thread synchronization is automatic – When a Task is created, the SynchronizationContext of the calling thread is captured if it’s available, which it will be for any GUI based application or ASP.NET application. If there is no SynchronizationContext, it will internally store a reference to an instance of TaskScheduler.Default. When the asynchronous operation completes and any continuations are executed, the continuation will automatically be marshaled to the captured context. If the captured context is a SynchronizationContext, then the continuations can interact with UI elements without the need to manually marshal the thread. If there is no SynchronizationContext, then a thread is acquired from the thread pool to run the continuation.
• Coordination with multiple asynchronous operations is easier – As we just saw in the example above, there are a number of extension methods on the Task class to combine one or more asynchronous operations into a larger operation that can be waited on or have its own continuation delegate specified. These task combinators, as they are called, make coordination between multiple asynchronous operations much more manageable.

Finally, if for some reason a class in the framework doesn’t offer a TAP version of an asynchronous operation you use frequently, it’s possible to wrap APM begin and end methods into a TAP model using the Task.FromAsync method. Let’s take a look at how we can wrap FileStream.BeginRead() and EndRead() into a method that implements the TAP model:

static void Main(string[] args)
{
//do other things while file is read

}
{
var filestream = File.OpenRead(filePath);
var readBuffer = new Byte[filestream.Length];
(Func<byte[], int, int, AsyncCallback, object, IAsyncResult>)filestream.BeginRead,
0,
(int)filestream.Length,
null);

{
Console.WriteLine("Read {0} bytes successfully from file {1}", task.Result, filePath);
else
Console.WriteLine("Exception occurred while reading file {0}.", filePath);

filestream.Dispose();
});

}

Pour Some Syntactic Sugar On Me

You may think to yourself the Task-Based asynchronous model still has the potential for readability and maintainability issues present in the APM model and you would be correct. So in C# 5, the async and await modifiers were added to work with the Task class to alleviate these issues and make the TAP method even more powerful. Think of these keywords as compiler support for implicit async continuations that can be written in a synchronous syntax. That’s technical speak for “awesomesauce.” Before we get deep into how async and await work, let’s again refactor our async file reading code to use TAP with async and await:

static void Main(string[] args)
{

//do stuff while file read is taking place.

}

{
var bytesRead = 0;
try
{                using (var fileStream = File.OpenRead(filePath))
{
var readBuffer = new Byte[fileStream.Length];
Console.WriteLine("Read {0} bytes successfully from file {1}", bytesRead, filePath);
}            }
catch(Exception)
{
Console.WriteLine("Exception occurred while reading file {0}.", filePath);

}
}

From a readability perspective the ReadFileAsync method looks synchronous. In fact we’re even able to wrap the operation in a using block, which wasn’t possible in either the APM or early TAP implementations, and that’s the beauty of the whole thing. Let’s see how the async and await keywords make this style of writing possible.

Firstly, notice the async modifier has been added to the signature of the ReadFileAsync method. Basically, the async modifier is nothing more than a hint to the compiler that the await keyword may be used within. If the modifier is not present, await cannot be called within the method body. It’s useful for developers too since it is a good way to help identify asynchronous methods if the async naming convention is not being followed. As before, we are returning a Task<int> here to represent the asynchronous operation taking place. We do this so callers of the method can also take part in the asynchrony going on in this method. If the async modifier is present on a method, that method can only return Task, Task<TResult>, or void. Task is returned if the synchronous signature would return void and Task<TResult> should be returned if the synchronous signature would return an instance of TResult. It is valid to return void from an async method as well but this comes with some caveats that will be covered a bit later in this article. Suffice it to say it should be used mostly for async event handlers and fire and forget scenarios.

private static async Task<int> ReadFileAsync(string filePath)
{            var bytesRead = 0;
try
{
using (var fileStream = File.OpenRead(filePath))                {
var readBuffer = new Byte[fileStream.Length];
Console.WriteLine("Read {0} bytes successfully from file {1}", bytesRead, filePath);
}
}            catch(Exception)
{
Console.WriteLine("Exception occurred while reading file {0}.", filePath);
}
}

It is advisable not to call Task.ConfigureAwait from GUI or ASP.NET applications to prevent the possibility of accidentally running into cross threading exceptions when trying to access UI elements or properties specific to a web request context.

One final note on the await modifier; it can be used zero or more times within a method that is marked with the async modifier. The compiler will warn you if await is not called within the method body as the method is run essentially synchronously at that point, but it’s also possible to await multiple other asynchronous operations within a method with the async modifier applied to it.

Beware Async Void

Recall one of the rules of using the async modifier on a method is it must return either Task, Task<TResult> or void. Returning void is a special use case with some potential pitfalls if not used carefully. Generally speaking, returning void from async methods is useful for event handlers that may need to perform asynchronous operations within. Since no Task is returned from these methods, there is no way to determine when the asynchronous operation completes, nor whether that operation was successful or not. Also, since the Task class absorbs exceptions thrown by the asynchronous operations they represent, async methods that return void will throw out to the caller if their asynchronous operations fail. If the calling code has no try/catch and no overarching AppDomain.UnhandledException or TaskScheduler.UnobservedTaskException handlers defined, these exceptions will end up killing the application. Below is an example of using async void to asynchronously read a file that doesn’t exist:

static void Main(string[] args)
{
}

private static async void ReadFileAsync()
{
var bytesRead = 0;
using (var fileStream = File.OpenRead(@"c:\somefilethatdoesnotexist.txt")) //bye bye application!            {                var readBuffer = new Byte[fileStream.Length];
}
}

What Makes Task So Awaitable?

The await modifier is not only applicable to the Task and Task<T> types and, in fact, doesn’t operate on types at all. Instead, await operates on an awaitable expression. These awaitable expressions aren’t tied to specific interfaces but rather implement a known pattern, much like the GetEnumerator method for enumerable types. There are a few rules that determine what qualifies as an awaitable expression:

• The type must contain a GetAwaiter() method. This method doesn’t have to be an instance method, and in fact it can even be an extension method, as it is for Task and Task<T>.
• The GetAwaiter() method must return an object that implements the INotifyCompletion interface. The object must also expose the following properties and methods:
• bool IsCompleted { get; }
• void OnCompleted(Action continuation)
• TResult GetResult()

The GetAwaiter() methods for Task and Task<TResult> return instances of TaskAwaitable and TaskAwaitable<TResult>, respectively. Both of these classes implement the ICriticalNotifyCompletion interface and expose implementations of the above methods and properties.

<blockquote>Note the GetResult() method of the TaskAwaitable object returns void as the Task class has no return type.</blockQuote>

Given this recipe, you can make your own awaitable classes for unique situations where you need explicit control over how work is scheduled on the thread pool or how the flow of asynchronous operations is decided.

The Await Is Over

Using the Task-based Asynchronous Programming model in conjunction with the async and await modifiers can make it significantly easier to write asynchronous code in our applications. The code we write is also more readable and less disconnected than using the legacy patterns. Check out these other async/await articles for more information about asynchronous programming and async/await:

Dave Marini has been involved in the design and development of enterprise e-commerce applications for the better part of the last decade. He specializes in Microsoft technologies and loves playing with new web technologies. He holds a BA in computer science from Boston University and lives in Connecticut. Recent writings include How Asynchronous Operations Can Reduce Performance and Simplifying Producer/Consumer Processing with TPL Dataflow Structures.

Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Tell us what you think

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Excellent Article!!!

All in one place. This gave me a very good understanding of Async programming

.NET 4.5 + Tasks + C# 5.0 provides a stronger TAP.

Extremely informative. When you explained the await expression, you clarified the continuations. This async/await compiler inference code generation process splits the async method into two separate code contexts. While the task returns immediately, the latter go until completion. The result is then placed into the task's result property. This avoids any drawbacks implementing the TAP, those that caused difficulties with the APM.
Close

by

on

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

2 Discuss

Login to InfoQ to interact with what matters most to you.