There are some common mistakes that one can see over and over again while reading an asynchronous code base. This mistakes range from egregious mistakes that can halt the whole process. To mistakes that create confusion and semantically incorrect code. So in this post, I’ll gather these common mistakes into a post with the top 7 most seen ones.
1- Avoid Using Async Void
Async void can produce lots of weird bugs and should be avoided except when we use it with event handlers. Here I explain two problems that can happen when we use async void.
First problem is that there is no way for the calling code to find out if the async operation is finished. Look at the example above, here we have the
Download method. Let’s ignore the fact that it lacks the async suffix. But the real problem is when we call
Download and reach the line that await something. What happen is we release the control back to the caller. At this point the caller goes to the next line and set the label1.Text. But the
_byteCount is still empty because the operation is still in progress. So there’s is not way for the
Download method to wait for the process to finish or to know when it will finish.
Here’s the correct way of doing this.
As you can see I changed the return type from void to Task, now the calling code can actually know when the operation is going to finish. It is even better if we return the value instead of mutating the field.
The second problem is with exception handling.
If you run this code, we never hit the lines that catch the exception and write the exception message. So by using async void you risk making exception handling very difficult. That’s because error-handling for async void method are different.
2- Don’t Mix Asynchronous Code With Synchronous
You’ve probably have seen async code that uses
SomethingAsync().Wait(). The problem with doing that is that you are blocking the calling thread. So by doing that you simply get no benefit at all by using asynchronous API.
What happens here is that when we call something async, the current context is captured. So we called the method and hit line 19, now we need to release the current thread back to the caller. But we can’t because we blocked the call until the asynchronous operation is finished. The thing with asynchronous programming is if you want to use an Asynchronous API the calling code should be Async. And the code that calls that caller should be asynchronous too. So this goes on across entire layers of your application. The solution is simple, if you want to use async make sure you do it all the way. So the above code excerpt could be corrected like so.
3- Using Synchronous Code in Asynchronous API
This problem is not as common but still happens. When we build an async method, it should be asynchronous in its entirety. Let me clarify what I mean by an example.
DownloadAsync is supposedly async but we block on line 20 and 21 by calling something synchronous. We can fix it by calling
DownloadDataTaskAsync on line 21 instead of
4- Don’t Create a Fake Asynchronous Method
What do I mean by fake? I mean when the method and its signature tell us the method is asynchronous while it is not.
In above code excerpt the
DownloadAsync method is no really async. What happens here is that
DownloadAsync method spawns up a new thread. So when we await this method we still have a busy thread. So we’re still blocking a thread pool thread. The key thing to understand is that truly asynchronous operations aren’t going to create a new thread. But here we do just that. To understand what I mean read There is no thread article here. The fix for this is to use the async API in the method’s body Instead of wrapping sync method with
Task.Run. Using Task.Run is ok as long as we use it explicitly for example in our user interface layer for naturally synchronous code. Maybe we need to use some sync API and we just wrap it inside
Task.Run to make our user interface responsive. You can read good article about it here and here.
5- Don’t Use Async When It Is Not Necessary
Some operations are naturally asynchronous, such as I/O operation. It can be extended to any operation that is not resource intensive, but getting result might need some time. In other words it has some latency. On the other hand some operations are naturally synchronous. Such as most computation or any code that does some calculation. Or maybe the code does something really trivial. Those operations are better be synchronous because not only making them async doesn’t improve the performance, but can create unnecessary overhead. Just take a look at what IL is generated for a synchronous operation vs asynchronous one.
6- Don’t Capture The Current Context When It’s Not Needed
When we await an asynchronous task, it going to capture the current SynchronizationContext before awaiting for the task to finish. We need to do that because when the task is finished, we want to return where we were. So we can use the result and do something with it or set the value of some UI element. This process has some cost which is not very important when it’s not repeated a lot. But it’s not always necessary. For example when we create a library, we don’t need to return to where we were before. In these situations we can use
await task.ConfigureAwait(false). So now when we use await, the current context is not captured, so we don’t waste resources unnecessarily. But the performance gain is not that noticeable, but it can become a problem when we’re capturing the context millions of times.
7- Watch For Heavy Synchronous Operation In Your Async Method
Mixing sync and async code becomes unavoidable sometimes. In these situations there is a danger the code blocks during execution of synchronous part of our code. Let me elaborate more by an code example.
What do you think is the output of this code? If your answer is “started”, “work”, “completed” then your answer is wrong. It actually going to output “work”, “started”, “completed”. The reason is that we start the operation by calling
WaitExampleAsync method and we intend to wait on it later. But what actually happens here is that we enter the
WaitExampleAsync body. When that happens we have some synchronous work in the body.
So basically the current thread is blocked by that long running operation until it’s done. Problem here is that we don’t want to wait for synchronous part of
WaitExampleAsync to be done. We want to do it asynchronously. In these situations we can use
Task.Yield to return the current thread back to the caller. This prevent the program from waiting for any synchronous operation in the body of our async method. So by uncommenting line 5, the program now prints “started”, “work”, “completed”.
In this post, I discussed some of the most common issues when using .Net asynchronous API. I also included examples that demonstrate how this mistakes can cause problems and what the fix to these problems are.