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...
Console.ReadLine(); } private static void OnReadComplete(IAsyncResult result) { var stream = (FileStream)result.AsyncState; var bytesRead = stream.EndRead(result); Console.WriteLine("Read {0} bytes successfully.", bytesRead); 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 => { var bytesRead = fs.EndRead(asyncResult); Console.WriteLine("Read {0} bytes successfully.", bytesRead); 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]; fs.ReadAsync(readBuffer, 0, (int)fs.Length) .ContinueWith(task => { if (task.Status == TaskStatus.RanToCompletion)
Console.WriteLine("Read {0} bytes successfully", task.Result); else Console.WriteLine("Exception occurred"); fs.Dispose(); }); //do other work here while file is read... Console.ReadLine(); }
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) { var read1 = ReadFileAsync(@"c:\somefile.txt "); var read2 = ReadFileAsync(@"c:\someotherfile.txt "); Task.WhenAll(read1, read2) .ContinueWith(task => Console.WriteLine("All files have been read successfully."));
//do other work here while files are read... Console.ReadLine(); } private static Task<int> ReadFileAsync(string filePath) { var fs = File.OpenRead(filePath); var readBuffer = new byte[fs.Length]; var readTask = fs.ReadAsync(readBuffer, 0, (int)fs.Length); readTask.ContinueWith(task => {
if (task.Status == TaskStatus.RanToCompletion) Console.WriteLine("Read {0} bytes successfully from file {1}", task.Result, filePath); else Console.WriteLine("Exception occurred while reading file {0}.", filePath); fs.Dispose(); }); return readTask; }
For this example, the file read operation has been refactored into its own method. With TAP, methods that participate in asynchronous behavior end with Async by convention. If a class already has a method with the desired name ending with Async, then it should end with TaskAsync to indicate this is the method that returns an instance of Task. The Task.WhenAll method wraps any tasks passed in as parameters into a larger asynchronous operation and returns a Task that represents the combined Asynchronous operation. In this case, we don’t wait for both files to be read but simply add a continuation for when both files have been read. If we needed to block the main thread until both files were read, we could have used the Task.WaitAll method instead.
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) { ReadFileAsync(@"c:\somefile.txt ");
//do other things while file is read Console.ReadLine(); }
private static Task<int> ReadFileAsync(string filePath) { var filestream = File.OpenRead(filePath); var readBuffer = new Byte[filestream.Length]; var readTask = Task.Factory.FromAsync( (Func<byte[], int, int, AsyncCallback, object, IAsyncResult>)filestream.BeginRead, (Func<IAsyncResult, int>)filestream.EndRead, readBuffer, 0, (int)filestream.Length, null); readTask.ContinueWith(task => { if (task.Status == TaskStatus.RanToCompletion) Console.WriteLine("Read {0} bytes successfully from file {1}", task.Result, filePath); else Console.WriteLine("Exception occurred while reading file {0}.", filePath); filestream.Dispose(); }); return readTask; }
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) { ReadFileAsync(@"c:\somefile.txt"); //do stuff while file read is taking place. Console.ReadLine(); } private static async Task<int> ReadFileAsync(string filePath) { var bytesRead = 0; try {
using (var fileStream = File.OpenRead(filePath)) { var readBuffer = new Byte[fileStream.Length]; bytesRead = await fileStream.ReadAsync(readBuffer, 0, (int)fileStream.Length);
Console.WriteLine("Read {0} bytes successfully from file {1}", bytesRead, filePath); return bytesRead; }
} catch(Exception) { Console.WriteLine("Exception occurred while reading file {0}.", filePath); return bytesRead; } }
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.
The await keyword triggers the real magic of the async/await pattern. When the compiler sees the await statement, it will create a continuation out of all of the code that follows the await statement. In our example above, that means the success Console.WriteLine statement as well as the disposal of the FileStream will be wrapped in a compiler callback that will work similarly to if we added them as a ContinueWith method call on the Task. At run time, when the instruction being awaited is encountered, the async operation is queried to determine if it has completed already. If it has, the rest of the method body after await is run synchronously just as it’s written in code. If, however, the operation hasn’t completed yet, then the async method is suspended and returns to the caller immediately, which allows the asynchronous operation to take place without holding up the calling thread. When the asynchronous operation is completed, the rest of the method body is resumed where it left off, either on the same thread that called it or on a thread pool thread. As mentioned above when we talked about TAP, the thread that runs the continuation logic depends on whether a SynchronizationContext is present or if the Task was configured to synchronize the continuation. For async/await scenarios, the compiler can be told not to synchronize the continuation thread by calling the ConfigureAwait method on the Task and passing false as the argument as in the following example:
private static async Task<int> ReadFileAsync(string filePath) {
var bytesRead = 0; try { using (var fileStream = File.OpenRead(filePath))
{ var readBuffer = new Byte[fileStream.Length]; bytesRead = await fileStream.ReadAsync(readBuffer, 0, (int)fileStream.Length).ConfigureAwait(false);
Console.WriteLine("Read {0} bytes successfully from file {1}", bytesRead, filePath); return bytesRead; } }
catch(Exception) { Console.WriteLine("Exception occurred while reading file {0}.", filePath); return bytesRead; } }
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) { ReadFileAsync();
Console.ReadLine(); } 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]; bytesRead = await fileStream.ReadAsync(readBuffer, 0, (int)fileStream.Length).ConfigureAwait(false);
Console.WriteLine("Read {0} bytes successfully from file.", bytesRead); } }
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:
Steven Cleary's excellent async/await post
Jon Skeet's eduasync series. A LOT of information here
About the Author
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.