You might have come across the two keywords async and await many times while working on .NET projects. Although it’s a fascinating thing to use, it’ll also be beneficial in knowing what happens under the hood when dealing with such code.
Before getting bogged down with in-depth details what’s what, we need to clarify some common misconceptions about Tasks and Threads used in C#.
🤔 Tasks Vs Threads
It is really important to understand that tasks are not an abstraction over threads, and we should think of tasks as an abstraction over some work that’s intended to be happening asynchronously. In summary:
Tasks are not Threads
Tasks are similar to Promises providing you with a clean API to handle async code
Tasks do not guarantee parallel execution
Tasks can be explicitly requested to run on a separate thread via the Task.Run API.
Tasks are scheduled to be run by a TaskScheduler
🔮 The Magic of the ‘await’ keyword
As Stephen Cleary clearly mentions in his excellent blog post, async keyword only enables await. So, an async method would simply run like any other synchronous method until it sees an await keyword.
The await keyword is where the magic happens. It gives back control to the caller of the method that performed await and it ultimately allows an IO-bound (eg. calling a web service) or a CPU bound task (eg: such as a CPU-heavy calculation) to be responsive. All async and await does is providing us with some nice syntactic sugar to write cleaner code.
Let’s consider the following simple example:
var client = new HttpClient();
var request = await client.GetAsync(url);
var download = await request.Content.ReadAsStringAsync();
The same code can be equivalent to what gets unfolded into under the hood:
var client = new HttpClient();
var request = client.GetAsync(url);
var download = request.ContinueWith(http =>
As you can see, it pretty much creates a chain of tasks that needs to execute and gets queued up with the TaskScheduler. The above code is somewhat okay, but it is also beneficial to always use the language constructs provided by C# without doing everything manually!
The sequence of how this gets executed is as follows:
Calling client.GetAsync(url) will create a request behind the scenes by calling lower-level .NET libraries.
Some part of its underlying code may run synchronously until it delegates its work from the networking APIs to the OS
At this point, a Task gets created and gets bubbled up to the original caller of the asynchronous code. This is still an unfinished Task!
During this time, the caller may query the status of the Task
Once the network request is completed by the OS level, the response gets returned via an IO completion port and CLR gets notified by a CPU interrupt that the work is done
The response gets scheduled to be handled by the next available thread to unwrap the data
The remainder of your async method continues running synchronously
The key takeaway here is that there won’t be any dedicated threads to complete a Task. Which also means that your task continuation/completion isn’t guaranteed to run on the same thread that started it.
☠️ Common gotchas when using async and await
You would be tempted to parallelise everything that you see in your code when you start using async and await (well, I did 😁). But there are some pitfalls that you should avoid in your code.
Making methods void methods async void
This is the first mistake I did when I got my hands dirty with async-await! I had a void method which called an async method within itself. Since you can’t use await without making the calling method to be async, I made the caller method async void !
The problem here is, the caller of the GetResult method doesn’t have any kind of control over the result of SomeAsyncMethod() This also causes other side-effects such as lack of a call stack to debug if something goes wrong, application crashes if an exception occurs etc.
However, there are times that you can’t avoid this though. If you open up AsyncWpfApp in our code example you would find code like the one below:
var output = await RunAsyncParallelDemo.Start();
It’s not necessarily a problem if you find code like the one above since you can’t change the method signatures of the event handlers in WPF.
Nevertheless, it’s recommended to avoid using async void where possible.
Ignoring or Forgetting to ‘await’
It is an easy mistake to forget to await on an async task since the compiler would happily compile your code without complaining. In the below example we simply call an async function without awaiting on it.
// Deliberately forgetting to await
DoSomeBackgroundWorkAsync() method will return a Task (either a Task or a Task depending on the implementation) to the caller method without actually executing it.
Therefore, be mindful when you are dealing with async methods (especially with third party libraries) not to forget to use await
Blocking async tasks with .Result() and .Wait()
This is another common trap developers jump into (unknowingly 😋) when they need to run some async code in a synchronous method. Let’s consider the below example:
var result = DoAsyncWork().Result();
At a glance, it may look pretty convenient to use the .Result() method. However, this could cause serious deadlock problems. In order to avoid this, you should usually make your caller method async. This could be lot of work as it creates a cascading effect on your changes. But that’s usually preferable.
More ways to write non-blocking code as clearly depicted in MSDN:
It’s recommended to make the caller async where you would end up making a lot of cascading changes to your codebase although it could be painful if you are working on a legacy project.
I hope you enjoyed this article as much as I did writing it. The bottom line is that Task is almost always the best option; it provides a much more powerful API and avoids wasting OS threads.
Ready for more stuff? Head over to my Github repo to work through some sample code.
Suggestions or Found a Bug?
If you think there’s a misunderstanding in any of the things I explained, please feel free to comment down below 🙂 Also, if you found a bug or have any suggestions, please open a pull request in my Github repo. Cheers!