The async and await keywords have been a part of C# language since version 5.0, which was released in autumn 2013 as part of Visual Studio 2013. Although in essence they make asynchronous programming simpler, one can still use them incorrectly.
Are you keeping up with new developer technologies? Advance your IT career with our Free Developer magazines covering C#, .NET Core, MVC, Azure, Angular, React, and more. Subscribe to this magazine for FREE and download all previous, current and upcoming editions.
This article describes some of these common mistakes, and gives advice on how to avoid them.
The Basics of C# Async and Await
As users, we prefer applications which respond quickly to our interactions, and do not “freeze” when loading or processing data. While it might still be acceptable for legacy desktop line of business applications to stop responding occasionally, we are less patient with mobile applications that keep us waiting. Even operating systems are becoming more responsive, and give you the option to terminate such misbehaving processes.
Figure 1: Windows warns about applications that stopped responding
If you have been a developer for a while, it is very likely that you faced scenarios where your application became unresponsive. One such example is a data retrieval operation that usually completed in a few hundred milliseconds at the most, suddenly took several seconds because of server or network connectivity issues. Since the call was synchronous, the application did not respond to any user interaction during that time.
To understand why this happens, we must take a closer look at how the operating system communicates with applications. Whenever the OS needs to notify the application of something, be it the user clicking a button or wanting to close the application; it sends a message with all the information describing the action to the application. These messages are stored in a queue, waiting for the application to process them and react accordingly.
Each application with a graphical user interface (GUI) has a main message loop which continuously checks the contents of this queue. If there are any unprocessed messages in the queue, it takes out the first one and processes it. In a higher-level language such as C #, this usually results in invoking a corresponding event handler. The code in the event handler executes synchronously. Until it completes, none of the other messages in the queue are processed. If it takes too long, the application will appear to stop responding to user interaction.
Asynchronous programming using async and await keywords provides a simple way to avoid this problem with minimal code changes. For example, the following event handler synchronously downloads an HTTP resource:
private void OnRequestDownload(object sender, RoutedEventArgs e)
{
var request = HttpWebRequest.Create(_requestedUri);
var response = request.GetResponse();
// process the response
}
Here is an asynchronous version of the same code:
private async void OnRequestDownload(object sender, RoutedEventArgs e)
{
var request = HttpWebRequest.Create(_requestedUri);
var response = await request.GetResponseAsync();
// process the response
}
Only three changes were required:
· The method signature changed from void to async void, indicating that the method is asynchronous, allowing us to use the await keyword in its body.
· Instead of calling the synchronous GetResponse method, we are calling the asynchronous GetResponseAsync method. By convention, asynchronous method names usually have the Async postfix.
· We added the await keyword before the asynchronous method call.
This changes the behavior of the event handler. Only a part of the method up to the GetResponseAsync call, executes synchronously. At that point, the execution of the event handler pauses and the application returns to processing the messages from the queue.
Meanwhile, the download operation continues in the background. Once it completes, it posts a new message to the queue. When the message loop processes it, the execution of the event handler method resumes from the GetResponseAsync call. First, its result of type Task<WebResponse> is unwrapped to WebResponse and assigned to the response variable. Then, the rest of the method executes as expected. The compiler generates all the necessary plumbing code for this to work.
Although Web applications on the server do not require their own special UI thread, they can still benefit from asynchronous programming. A dedicated thread processes each incoming request. While this thread is busy with one request, it cannot start processing another one. Since there is a limited number of threads available in the thread pool, this limits the number of requests that can be processed in parallel. Any thread waiting for an I/O operation to complete, is therefore a wasted resource. If the I/O operation is performed asynchronously instead, the thread is not required any more until the operation completes, and is released back to the thread pool, making it available to process other requests. Although this might slightly increase the latency of a single request, it will improve the overall throughput of the application.
The use of async and await keywords is not limited to asynchronous programming though. With Task Parallel Library (TPL) you can offload CPU intensive operations onto a separate thread by calling Task.Run. You can await the returned task the same way as you would with an asynchronous method to prevent blocking the UI thread. Unlike real asynchronous operations, the offloaded work still consumes a thread; therefore, this strategy is not as useful in web applications where there is no special thread to keep available.
C# Async Await Asynchronous Programming - Common Pitfalls
The async and await keywords provide a great benefit to C# developers by making asynchronous programming easier. Most of the times, one can use them without having to understand the inner workings in detail. At least, as long as the compiler is a good enough validator, the code will behave as intended. However, there are cases when incorrectly written asynchronous code will compile successfully, but still introduce subtle bugs that can be hard to troubleshoot and fix.
Let us look at some of the most common pitfall examples.
Avoid Using Async Void
The signature of our asynchronous method in the code example we just saw was async void.
While this is appropriate for an event handler and the only way to write one, you should avoid this signature in all other cases. Instead, you should use async Task or async Task<T> whenever possible, where T is the return type of your method.
As explained in the previous example, we need to call all asynchronous methods using the await keyword, e.g.:
DoSomeStuff(); // synchronous method
await DoSomeLengthyStuffAsync(); // long-running asynchronous method
DoSomeMoreStuff(); // another synchronous method
This allows the compiler to split the calling method at the point of the await keyword. The first part ends with the asynchronous method call; the second part starts with using its result if any, and continues from there on.
In order to use the await keyword on a method; its return type must be Task. This allows the compiler to trigger the continuation of our method, once the task completes. In other words, this will work as long as the asynchronous method’s signature is async Task. Had the signature been async void instead, we would have to call it without the await keyword:
DoSomeStuff(); // synchronous method
DoSomeLengthyStuffAsync(); // long-running asynchronous method
DoSomeMoreStuff(); // another synchronous method
The compiler would not complain though. Depending on the side effects of DoSomeLengthyStuffAsync, the code might even work correctly. However, there is one important difference between the two examples. In the first one, DoSomeMoreStuff will only be invoked after DoSomeLengthyStuffAsync completes. In the second one, DoSomeMoreStuff will be invoked immediately after DoSomeLengthyStuffAsync starts. Since in the latter case DoSomeLengthyStuffAsync and DoSomeMoreStuff run in parallel, race conditions might occur. If DoSomeMoreStuff depends on any of DoSomeLengthyStuffAsync’s side effects, these might or might not yet be available when DoSomeMoreStuff wants to use them. Such a bug can be difficult to fix, because it cannot be reproduced reliably. It can also occur only in production environment, where I/O operations are usually slower than in development environment.
To avoid such issues altogether, always use async Task as the signature for methods you intend to call from your code. Restrict the usage of async void signature to event handlers, which are not allowed to return anything, and make sure you never call them yourself. If you need to reuse the code in an event handler, refactor it into a separate method returning Task, and call that new method from both the event handler and your method, using await.
Beware of Deadlocks
In a way, asynchronous methods behave contagiously. To call an asynchronous method with await, you must make the calling method asynchronous as well, even if it was not async before. Now, all methods calling this newly asynchronous method must also become asynchronous. This pattern repeats itself up the call stack until it finally reaches the entry points, e.g. event handlers.
When one of the methods on this path to the entry points cannot be asynchronous, this poses a problem. For example, constructors. They cannot be asynchronous, therefore you cannot use await in their body. As discussed in the previous section, you could break the asynchronous requirement early by giving a method async void signature, but this prevents you from waiting for its execution to end, which makes it a bad idea in most cases.
Alternatively, you could try synchronously waiting for the asynchronous method to complete, by calling Wait on the returned Task, or reading its Result property. Of course, this synchronous code will temporarily stop your application from processing the message queue, which we wanted to avoid in the first place. Even worse, in some cases you could cause a deadlock in your application with some very innocent looking code:
private async void MyEventHandler(object sender, RoutedEventArgs e)
{
var instance = new InnocentLookingClass();
// further code
}
Any synchronously called asynchronous code in InnocentLookingClass constructor is enough to cause a deadlock:
public class InnocentLookingClass()
{
public InnocentLookingClass()
{
DoSomeLengthyStuffAsync().Wait();
// do some more stuff
}
private async Task DoSomeLengthyStuffAsync()
{
await SomeOtherLengthyStuffAsync();
}
// other class members
}
Let us dissect what is happening in this code.
MyEventHandler synchronously calls InnocentLookingClass constructor, which invokes DoSomeLengthyStuffAsync, which in turn asynchronously invokes SomeOtherLengthyStuffAsync. The execution of the latter method starts; at the same time the main thread blocks at Wait until DoSomeLengthyStuffAsync completes without giving control back to the main message loop.
Eventually SomeOtherLengthyStuffAsync completes and posts a message to the message queue implying that the execution of DoSomeLengthyStuffAsync can continue. Unfortunately, the main thread is waiting for that method to complete instead of processing the messages, and will therefore never trigger it to continue, hence waiting indefinitely.
As you can see, synchronously invoking asynchronous methods can quickly have undesired consequences. Avoid it at all costs; unless you are sure what you are doing, i.e. you are not blocking the main message loop.
Allow Continuation on a Different Thread
The deadlock in the above example would not happen if DoSomeLengthyStuffAsync did not require to continue on the main thread where it was running before the asynchronous call. In this case, it would not matter that this thread was busy waiting for it to complete, and the execution could continue on another thread. Once completed, the constructor execution could continue as well.
As it turns out, there is a way to achieve this when awaiting asynchronous calls – by invoking ConfigureAwait(false) on the returned Task before awaiting it:
await SomeOtherLengthyStuffAsync().ConfigureAwait(false);
This modification would avoid the deadlock, although the problem of the synchronous call in the constructor, blocking the message loop until it completes, would remain.
While allowing continuation on a different thread in the above example might not be the best approach, there are scenarios in which it makes perfect sense. Switching the execution context back to the originating thread affects performance, and as long as you are sure that none of the code after it resumes needs to run on that thread, disabling the context switch will make your code run faster.
You might wonder which code requires the context to be restored. This depends on the type of the application:
· For user interface based applications (Windows Forms, WPF and UWP), this is required for any code that interacts with user interface components.
· For web applications (ASP.NET), this is required for any code accessing the request context or authentication information.
When you are unsure, you can use the following rule of thumb, which works fine in most cases:
· Code in reusable class libraries can safely disable context restoration.
· Application code should keep the default continuation on the originating thread – just to be on the safer side.
Planned Features for C# 7
The language designers succeeded in making async and await very useful with its first release in 2013. Nevertheless, they are constantly paying attention to developer feedback, and try to improve the experience wherever and whenever it makes sense.
For example, in C# 6.0 (released with Visual Studio 2015) they added support for using await inside catch and finally blocks. This made it easier and less error prone to use asynchronous methods in error handling code.
According to plans (which might however still change), C# 7 will have two new features related to asynchronous programming.
Note: For those new to C# 7, make sure to check out C# 7 – Expected Features
Support for Async Main
In the section about deadlocks, I explained how making a method asynchronous has a tendency of propagating all the way to their entry points. This works out fine for event driven frameworks (such as Windows Forms and WPF) because event handlers can safely use async void signature, and for ASP.NET MVC applications, which support asynchronous action methods.
If you want to use asynchronous methods from a simple console application, you are on your own. Main method as the entry point for console applications must not be asynchronous. Hence to call asynchronous methods in a console application, you need to create your own top-level asynchronous wrapper method and call it synchronously from Main:
static void Main()
{
MainAsync().Wait();
}
Since there is no message loop to block in a console application, this code is safe to use without the danger of causing a deadlock.
Language designers are considering the idea of adding support for asynchronous entry points for console applications, directly into the compiler. This would make any of the following method signatures valid entry points for console applications in C# 7:
// current valid entry point signatures
void Main()
int Main()
void Main(string[])
int Main(string[])
// proposed additional valid entry point signatures in C# 7
async Task Main()
async Task<int> Main()
async Task Main(string[])
async Task<int> Main(string[])
While this feature might not enable anything that is not already possible, it will reduce the amount of boilerplate code and make it easier for beginners to call asynchronous methods correctly from console applications.
Performance Improvements
I have already discussed the impact of context switching on asynchronous programming with async and await. Another important aspect are memory allocations.
Each asynchronous method allocates up to three objects on the heap:
- the state machine with method’s local variables,
- the delegate to be called on continuation, and
- the returned Task.
Since additional object allocations boils down to more work for the garbage collector, the current implementation is already highly optimized. The first two allocations only happen when they are required, i.e. when another asynchronous method is actually awaited. E.g. this scenario would only occur to the following method when called with true:
private async Task DoSomeWorkAsync(bool doRealWork)
{
if (doRealWork)
{
await DoSomeRealWorkAsync();
}
}
This is a contrived example, but even real-world methods often include edge cases with different execution paths, which might skip all asynchronous calls in them.
The allocation of the returned task is also already somewhat optimized. Common Task objects (for values 0, 1, true, false, etc.) are cached to avoid allocating a new one whenever one of these commonly used values are returned.
C# 7 promises to bring this optimization a step further. Asynchronous methods returning value types will be able to return ValueTask<T> instead of Task<T>. As the name implies, unlike Task<T>, ValueTask<T> is itself a struct, i.e. a value type that will be allocated on the stack instead of on the heap. This will avoid any heap allocations whatsoever for asynchronous methods returning value types, when they make no further asynchronous calls.
Apart from the benefit for garbage collection, initial tests by the team also show almost 50% less time overhead for asynchronous method invocations, as stated in the feature proposal on GitHub. When used in tight loops, all of this can add up to significant performance improvements.
Conclusion:
Even though asynchronous programming with C# async and await seems simple enough once you get used to it, there are still pitfalls to be aware of. The most common one is improper use of async void, which you can easily overlook and the compiler will not warn you about it either. This can introduce subtle and hard to reproduce bugs in your code that will cost you a lot of time to fix. If being aware of that remains your only takeaway from this article, you have already benefited. Of course, learning about the other topics discussed in this article, will make you an even better developer.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Damir Arh has many years of experience with software development and maintenance; from complex enterprise software projects to modern consumer-oriented mobile applications. Although he has worked with a wide spectrum of different languages, his favorite language remains C#. In his drive towards better development processes, he is a proponent of Test-driven development, Continuous Integration, and Continuous Deployment. He shares his knowledge by speaking at local user groups and conferences, blogging, and writing articles. He is an awarded Microsoft MVP for .NET since 2012.