Many applications have functionality that requires it to, for one reason or another, wait on something to complete. This could be a process, calculation, web request, or some other input/output operation. Instead of waiting synchronously for such an operation to complete, which could be inefficient and/or cause your application to appear frozen, it is a common requirement to perform such waiting asynchronously such that your application remains responsive and capable of doing other things during the wait. In C#, using async
and await
is the primary way of doing such asynchronous waiting, and this guide will help you get started with that. But as mentioned in the previous guide in this series, it is possible to do asynchronous programming in C# without these keywords. So why were these keywords introduced, and why are they so important?
To fully appreciate what the async
/await
approach provides, let's first write some asynchronous code without it. Let's imagine that we want a simple console application that downloads and blurs an image, and we want to perform the downloading and blurring asynchronously. By maintaining a "dashboard" of sorts with the current time in milliseconds, we can demonstrate that the application remains responsive throughout its execution.
1static bool done = false;
2
3static void Main()
4{
5 DownloadAndBlur();
6
7 while (!done)
8 {
9 Console.CursorLeft = 0;
10 Console.Write(DateTime.Now.ToString("HH:mm:ss.fff"));
11 Thread.Sleep(50);
12 }
13
14 Console.WriteLine();
15 Console.WriteLine("Done!");
16}
Now, let's tackle the downloading and blurring. The .NET Task Parallel Library provides support for asynchronous waiting via an abstraction called a Task
, which can be used regardless of the type of operation that you need to perform. If we structure our code to take advantage of Task
, we might have a C# method for each operation.
1static Task<byte[]> DownloadImage(string url) { ... }
2
3static Task<byte[]> BlurImage(string imagePath) { ... }
4
5static Task SaveImage(byte[] bytes, string imagePath) { ... }
Finally, we need to define DownloadAndBlur
, which will invoke the above methods using the .NET Task Parallel Library.
1static void DownloadAndBlur()
2{
3 var url = "https://...jpg";
4 var fileName = Path.GetFileName(url);
5 DownloadImage(url).ContinueWith(task1 =>
6 {
7 var originalImageBytes = task1.Result;
8 var originalImagePath = Path.Combine(ImageResourcesPath, fileName);
9 SaveImage(originalImageBytes, originalImagePath).ContinueWith(task2 =>
10 {
11 BlurImage(originalImagePath).ContinueWith(task3 =>
12 {
13 var blurredImageBytes = task3.Result;
14 var blurredFileName = $"{Path.GetFileNameWithoutExtension(fileName)}_blurred.jpg";
15 var blurredImagePath = Path.Combine(ImageResourcesPath, blurredFileName);
16 SaveImage(blurredImageBytes, blurredImagePath).ContinueWith(task4 =>
17 {
18 done = true;
19 });
20 });
21 });
22 });
23}
Because of our use of Task
, invocation of each method returns immediately, before the "task" (i.e. operation) to which it corresponds has completed. Likewise, ContinueWith
defines an additional task that corresponds to what should happen when the previous task completes. As a result, the execution of the DownloadAndBlur
method finishes extremely quickly, and the "dashboard" begins updating as soon as you start running the application. In short, everything is done in a non-blocking, asynchronous fashion, and it's all thanks to the .NET Task Parallel Library.
But you may notice that the code in DownloadAndBlur
is a bit hard to follow, due to its use of callbacks. For each task, we have to add a level of indentation, along with the parentheses and braces that accompany any method. An unfortunate side effect of this approach is reduced readability. What's more, it is difficult to discern at first glance that this code is asynchronous. Obviously reading code is not only about understanding words; it's also about understanding the flow of the code as it is executed at run-time. Fortunately, C# developers do have an approach available to them that improves on both of these aspects.
Let's rewrite the DownloadAndBlur
method, but this time with async
and await
.
1static async void DownloadAndBlur()
2{
3 var url = "https://...jpg";
4 var fileName = Path.GetFileName(url);
5 var originalImageBytes = await DownloadImage(url);
6 var originalImagePath = Path.Combine(ImageResourcesPath, fileName);
7 await SaveImage(originalImageBytes, originalImagePath);
8 var blurredImageBytes = await BlurImage(originalImagePath);
9 var blurredFileName = $"{Path.GetFileNameWithoutExtension(fileName)}_blurred.jpg";
10 var blurredImagePath = Path.Combine(ImageResourcesPath, blurredFileName);
11 await SaveImage(blurredImageBytes, blurredImagePath);
12 done = true;
13}
Notice how there are no longer any callbacks and, therefore, the corresponding indentation, braces, and parentheses are no longer needed. There are also no more references to ContinueWith
, resulting in code that is much easier to read and understand. In fact, most of what we had to change about the DownloadAndBlur
method was removing code. The main thing that was added was an await
keyword in front of each method returning Task
. So, not only is using async
/await
more readable, but it is also easier to write!
As you might have intuited, await
causes execution to pause in a non-blocking fashion until the corresponding operation finishes, then continue with the code that succeeds it. It's important to note that the method you wish to await must return a Task
. (In that sense a Task
in C# is analogous to a Promise
in modern JavaScript.) If you attempt to await something that does not return Task
, your code will not compile. Given that the await
keyword is fairly straightforward, the main challenge about making your code asynchronous in C# is actually ensuring your code is structured to use Task
. Once you have done that, adding await
is not so difficult.
But what about the async
keyword? We added that to the signature of the DownloadAndBlur
method between static
and void
, but is that really needed?
It can be easy to forget to add the async
keyword to the signatures of methods that use await
, but it is a requirement. There is, however, a good reason for that. Prior to the introduction of the await
keyword in C# 5.0, you could use await
as an identifier name, for variables and the like. While, in many cases, the compiler can discern the intended usage based on the context, in order to retain full backward compatibility the async
keyword was introduced to eliminate ambiguity. If the async
keyword is present in the method signature, the compiler knows to interpret await
as a C# keyword. Conversely, if the async
keyword is not present the compiler allows you to name variables await
if you so choose.
All of that being the case, it's understandable that not every method that returns a Task
requires the async
keyword in its signature. Again, async
is only needed in the method signature if await
is used inside that particular method's code. You can create and return tasks without awaiting them; a method that does so would not use the async
keyword. But don't stress too much about the nuances! Normally the compiler can figure out what you meant and remind you to add async
if necessary, or remove it if you're not awaiting anything.
Another context in which you might see the word "async" is in the method name itself. For example, in the HttpClient
class in the System.Net.Http
namespace, there is a method with the following signature:
1public Task<string> GetStringAsync (string requestUri)
It's important to know that "Async" in this context is not a keyword, but merely a naming convention; it has no meaning for the compiler. They could have just as easily named the method GetString
and it would function in exactly the same way. Feel free to suffix your own asynchronous methods with "Async". Some find that naming convention helpful, while others find it distracting. Make your own decision about that; it is completely optional!
In addition to the tremendous improvements in code readability that we talked about above, there are a number of interesting advantages to using the async
/await
approach that make it appealing. If you use Task
with ContinueWith
, exceptions thrown will not escape the task; you have to instead inspect properties of the Task
when it completes, including the InnerExceptions
property of the AggregateException
that the completed Task
exposes to you. By contrast, when you await
a method, you can use try
/catch
normally and it will catch exceptions as one would expect. Not only that, any exceptions caught will be automatically "unwrapped" to the actual exception, as opposed to any sort of aggregate holder. So you can safely catch (FormatException)
, or whatever the case might be.
In addition, if you use Task
with ContinueWith
, you need to have some knowledge of the TaskScheduler
used by the first task in order to fully understand how subsequent continuation tasks will run by default. Alternatively, you can explicitly specify the scheduler you want to use for the continuation tasks, though doing so adds additional verbosity to your code. This is an advanced topic, but suffice it to say that there are some tricky issues one can run into that can be avoided by using the async
/await
approach instead. If you're not doing anything particularly unusual, using await
is far more straightforward.
Of course, there are some trade-offs when using async
/await
, but the same can be said for asynchronous programming in general. If you compare asynchronous programming to synchronous programming, asynchronous programming gives you increased responsiveness at the expense of increased complexity, higher memory usage, and longer overall execution time. Using async
/await
does indeed have these drawbacks. In particular, the state machine it uses adds a decent amount of overhead. But for most C# developers, the benefits of async
/await
make it well worth the cons.
As you begin using the async
and await
keywords you will probably find that they are not that difficult to use. But they do require a new way of writing and reading code, as an application's control flow is harder to understand when it is non-sequential. It is important, then, to know a bit about what happens under the hood when we await
a method. In the next guide in this series, we'll take a deeper look at control flow with async
and await
.