In the previous guide in this series we took a look at the basics of the async
and await
keywords in C#. Once you get the hang of their syntax and usage, it can actually be quite a joy to write asynchronous code. In fact, it feels so natural that one can forget that the code is asynchronous! Often this is an advantage; you can ignore the minutiae and focus on the application you're building. But, sooner or later, you will come across some confusing behavior that will remind you how tricky asynchronicity can be. It's at these moments that understanding what happens under the hood when using async
and await
becomes important. It turns out that there is a lot going on.
You might recall from the previous guide that the async
keyword is actually just a way to eliminate ambiguity for the compiler with regard to await
. So, when we talk about the async
/await
approach, it's really the await
keyword that does all the heavy lifting. But before we look at what await
does, let's talk about what it does not do.
The await
keyword does not block the current thread. What do we mean by that? Let's look at some examples of blocking code.
1System.Threading.Thread.Sleep(1000);
The above code blocks execution of the current thread for one second. Other threads in the application may continue to execute, but the current thread does absolutely nothing until the sleep operation has completed. Another way to describe it is that the thread waits synchronously. Now, for another example, this time from the Task Parallel Library:
1var httpClient = new HttpClient();
2var myTask = httpClient.GetStringAsync("https://...");
3var myString = myTask.GetAwaiter().GetResult();
In the above code snippet .NET's HttpClient
class is returning a Task
instance, but we're calling GetAwaiter().GetResult()
on the task, which is a blocking call. Again, this is synchronous; no execution will take place on the current thread until GetResult
returns with the data returned by the operation (the requested string data in this example). Similarly, a task's Result
property also blocks synchronously until data is returned.
Last but not least, there's also a Wait
method that is blocking, e.g.:
1myTask.Wait();
Even if the underlying task is asynchronous, if you call a blocking method or blocking property on the task, execution will wait for the task to complete - but will do so synchronously, such that the current thread is completely occupied during the wait. So, if you use one of the above properties/methods, be sure that's actually what you meant to do.
The await
keyword, by contrast, is non-blocking, which means the current thread is free to do other things during the wait. But what else would the current thread be doing?
To better answer the question about what a thread would do during a non-blocking wait, it might be helpful to take a step back and think about asynchronicity in general from the perspective of an application user. As an example: In previous guides, we've looked at the code for an application that downloads and blurs images. Let's say this time that the application has a graphical user interface with two buttons and a progress bar. The first button downloads and blurs images while displaying a progress bar animation and the second button counts the number of images you've blurred and tells you the number in some sort of dialog.
Now, imagine you are the user and the developer has used code that is blocking or synchronous. You click the first button to download and blur the images and, when you do, the application appears to freeze. The progress bar either doesn't show up at all or it appears with a frozen animation. Perhaps your mouse cursor changes to an hourglass or a pinwheel. You can't even move the application window, much less click the second button. Short of killing the application, you have no choice but to wait for the download/blur operation to finish.
Let's contrast that with the user experience if the same application were written to use await
. You click the first button and the progress bar appears with an animation. While waiting, you decide to move the application window, which repaints itself smoothly as you drag the window. Still waiting, you decide to click the second button and get an image count in the meantime. Before long, the image count appears in a dialog. Finally, the download/blur operation finishes and the progress bar hides itself.
As you can see, the user provided plenty for the original thread to do while awaiting the long-running operation. The fact that await
frees the thread up to do other things means that it can remain responsive to additional user actions and input. But, even if there is no graphical user interface, we can see the advantage of freeing up a thread. As was demonstrated in previous guides in this series, a console application can also display progress independent of execution in the form of a text-based dashboard; you could extend that idea to periodically check for keyboard input. In the case of ASP.NET, freeing up a thread potentially means greater scalability, allowing a single server to handle more requests concurrently than it otherwise could. It can, therefore, be very advantageous to write non-blocking code with await
.
We know now that await
doesn't block - it frees up the calling thread. But how does this non-blocking behavior manifest itself to the calling method? Consider the following code.
Assume there is a method somewhere called
ShowDialog
that shows a message alert of some sort to the user.
1void OnButtonClick()
2{
3 DownloadAndBlur("https://...jpg");
4 ShowDialog("Success!");
5}
6
7async Task DownloadAndBlur(string url)
8{
9 await DownloadImage(...);
10 await BlurImage(...);
11 await SaveImage(...);
12}
If you were to run this code you would notice a problem: The success dialog displays before the download/blur operation completes! This demonstrates an important point: When a method using await
is not itself awaited, execution of the calling method continues before the called method has completed. Let's add some logging to see that in detail:
1void OnButtonClick()
2{
3 Console.WriteLine("button clicked");
4 DownloadAndBlur("https://...jpg");
5 Console.WriteLine("about to show dialog");
6 ShowDialog("Success!");
7 Console.WriteLine("dialog shown");
8}
9
10async Task DownloadAndBlur(string url)
11{
12 Console.WriteLine("about to download");
13 await DownloadImage(...);
14 Console.WriteLine("finished downloading, about to blur");
15 await BlurImage(...);
16 Console.WriteLine("finished blurring, about to save");
17 await SaveImage(...);
18 Console.WriteLine("finished saving");
19}
The output is as follows:
1button clicked
2about to download
3about to show dialog
4dialog shown
5finished downloading, about to blur
6finished blurring, about to save
7finished saving
Notice that, at first, execution of DownloadAndBlur
is performed synchronously, until the first encounter with await
. Control at that time returns to the calling method as if DownloadAndBlur
had already finished.
Note that control may not return to the calling method immediately, but will do so at the first opportunity. For example, if the user clicks another button at the right moment the application's main thread might already be busy with something else. Whenever the thread is next idle, control will resume in the calling method as described.
We can, of course, fix the above example by using await
(and don't forget async
) in OnButtonClick
as follows:
1async void OnButtonClick()
2{
3 Console.WriteLine("button clicked");
4 await DownloadAndBlur("https://...jpg");
5 Console.WriteLine("about to show dialog");
6 ShowDialog("Success!");
7 Console.WriteLine("dialog shown");
8}
But a larger question remains. In all cases, at some point after an await
a method needs to "wake up", as it were, and continue with the rest of its code. How exactly does execution resume a piece of a method?
To answer that question, let's try logging the call stack in DownloadAndBlur
. One way to do that is:
1Console.WriteLine(new System.Diagnostics.StackTrace());
You'll see something like the following:
1at OnButtonClick()
2 at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
3 at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext()
4 at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(IAsyncStateMachineBox box, Boolean allowInlining)
5 at System.Threading.Tasks.Task.RunContinuations(Object continuationObject)
6 at System.Threading.Tasks.Task`1.TrySetResult(TResult result)
7 at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetExistingTaskResult(TResult result)
8 at System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetResult()
9at DownloadAndBlur()
10 at System.Threading.ExecutionContext.RunInternal...
Notice that there are a lot of methods being called here that we did not define in our code, including AsyncStateMachineBox.MoveNext()
and AsyncTaskMethodBuilder.SetResult
. It's apparent that the compiler is generating a bunch of code on our behalf to keep track of the execution state.
The details of the generated code are outside the scope of this guide (and vary depending on the C# compiler and version), but suffice it to say that there is a state machine produced that uses goto
statements in combination with the Task Parallel Library methods, along with some exception and context (i.e. thread) tracking. If you're interested in going deeper, try inspecting a .NET assembly that includes await
in a .NET decompiler capable of decompiling to C#. In this manner, you can see every detail!
Although control flow when it comes to exception handling is exactly as one would expect when using await
, it's worth repeating that the opposite is not the case. Consider the following code:
1async void OnButtonClick
2{
3 string imageUrl = null;
4 try
5 {
6 DownloadAndBlur(imageUrl);
7 }
8 catch (Exception ex)
9 {
10 Console.WriteLine($"Exception: {ex}");
11 }
12 Console.WriteLine("Done!");
13}
14
15async Task DownloadAndBlur(string url)
16{
17 if (url == null)
18 {
19 throw new ArgumentNullException(nameof(url));
20 }
21 ...
22}
One might expect the output to include Exception: ArgumentNullException
. However, not only is that not the case, unless you have a debugger attached that pauses on all exceptions you won't know that an exception took place at all! That problem does not arise when using await
however. So unless you have a good reason not to do so, it is best to await
all of your asynchronous methods.
But, you might ask, what about what's often called "fire and forget"? This refers to the situation where you do not want to await
an asynchronous method and you're not particularly concerned about when it finishes. In that case, consider, at the very least, adding a ContinueWith
with TaskContinuationOptions.OnlyOnFaulted
where you can log any exceptions that may arise. Even better, go ahead and await
the method, but make the method call the very last thing you do in your outermost method. That way, none of your other code will have its execution postponed while still taking advantage of the exception treatment that comes with using await
.
Although the control flow of an application is always a bit tricky when it has asynchronous aspects, knowing a bit about how await
works under the hood helps a great deal. The most important thing about the await
keyword though is to use it. As you observe your application's behavior and troubleshoot edge cases, the control flow of await
introduced to you in this guide will be reinforced in your mind and your confidence with regard to this powerful tool will grow.
So far in this series, we have talked mainly about using await
with I/O operations, but computationally expensive operations require something extra. The next guide in this series will explore this topic.
Explore these Async and Await courses from Pluralsight to continue learning: