Since the introduction of the Task-based Asynchronous Pattern (or TAP) with .NET 4.0 programmers have enjoyed a simpler and streamlined approach to asynchronous programming in .NET, improving performance and readability of their code.
If you are already familiar with .NET TAP basic concepts, don’t miss the opportunity to learn more from an expert: Brandon Minnick, developer advocate with Microsoft, will address some in his talk “Correcting Common Async/Await Mistakes in .NET” at Codemotion Milan 2019 – check the agenda. Hurry up as tickets are still available!
Before TAP, .NET programmers had several alternatives to write non-blocking code:
End*methods and its
All of these alternatives are somewhat cumbersome and difficult to handle compared to the Async/Await paradigm introduced with TAP. They all require a lot of boilerplate just to get things up and running, with the obvious consequence of hurting readability and maintainability.
TAP is instead very simple to grasp, as far as parallel programming can go. It is modelled around the concept of Tasks as the name implies. A Task is simply a generic asynchronous operation, some piece of code we want to run without blocking the caller or other tasks.
So let’s get started with a simple “hello world” example with the Async/Await constructs. The goal of this simple program is to perform a few HTTP GET requests against a list of servers. A simple synchronous version would be:
getDataSync() performs an HTTP request using the
HttpClient class in
System.Net.Http and returns the result as a
String. The method
DoWork() calls the method
getDataSync() for all the websites in the list.
There is an obvious problem with this implementation: throughput. Requests are performed one after the other in sequence. This hurts the overall performance since our program is blocked waiting for a request to complete before attempting to perform anything else. Moreover, if an error occurs while performing one of the GET calls, the entire sequence is interrupted. This is often an undesired behaviour, as in case of errors we want to be able to gracefully rollback or continue with the other calls.
Lastly, the main thread is also blocked for the entire time, making the program completely unresponsive. If this piece of code would be part of a desktop application, the GUI would freeze until all the requests are finally completed.
So let’s change the code to perform all the requests asynchronously and in a parallel fashion. To turn a normal method to an asynchronous one we must perform these changes:
asynckeyword to its definition. This enables the usage of the
awaitoperator inside that method. The
awaitoperator will suspend the execution of the
asyncmethod until the asynchronous operation represented by its operand is completed;
Tis the return data type;
Here is how the
getDataAsync() method looks like:
While this will execute the requests in different Tasks, they will still be in a sequence, because of the await in the
foreach loop. A better approach would be to let the Tasks run in parallel and let the
DoWork() method return when they have all completed their mission. This can be easily achieved with the
Task.WaitAll() method. To do so, we create an array of Tasks, populate it with the Tasks returned by each call of the
getDataSync() method and then pass the array to
Task.WaitAll() to wait for them:
Or there is not. So far we assumed things are always going smoothly. But what happens if an exception is thrown inside one of the async methods? Let’s simulate this condition by adding a non existing URL to the list of sites to request. This will obviously make the
HttpClient throw an
HttpRequestException since the DNS request will fail.
As expected, an exception is thrown. The interesting fact here is that the exception is thrown by the
Task.WaitAll() method, and it is a
System.AggregateException. That is because the .NET framework will automatically surround our aysnc methods with try/catch blocks to let the Task awaiter handle them. The exception thrown by the
SocketException in this case, can be accessed as an
InnerException. Let’s handle these exceptions in the
What if we want to manage the exception in the
getDataAsync() method instead? As for any other method, we can simply wrap the await call inside a try/catch block:
This works as expected because
HttpClient.GetStringAsync() method will assign any exception that occurs during its execution to the returning Task.
There is a lot more to cover about asynchronous programming with .NET, considering best practices, optimization and exception handling. There are several odd cases in which exception handling may give you some headaches when dealing with async methods.
Again, if you want to learn more about it, don’t miss the talk Brandon Minnick, developer advocate with Microsoft, will give at Codemotion Milan 2019 where he will address more advanced topics and show some interesting examples. Get your ticket here!