Author avatar

Nate Cook

Getting Started with C#'s Async and Await Keywords

Nate Cook

  • Feb 5, 2019
  • 11 Min read
  • 6,751 Views
  • Feb 5, 2019
  • 11 Min read
  • 6,751 Views
C#
Keywords

The Need for Asynchronicity

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?

Asynchronous Programming Without Async and Await

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static bool done = false;

static void Main()
{
  DownloadAndBlur();

  while (!done)
  {
    Console.CursorLeft = 0;
    Console.Write(DateTime.Now.ToString("HH:mm:ss.fff"));
    Thread.Sleep(50);
  }

  Console.WriteLine();
  Console.WriteLine("Done!");
}
csharp

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.

1
2
3
4
5
static Task<byte[]> DownloadImage(string url) { ... }

static Task<byte[]> BlurImage(string imagePath) { ... }

static Task SaveImage(byte[] bytes, string imagePath) { ... }
csharp

Finally, we need to define DownloadAndBlur, which will invoke the above methods using the .NET Task Parallel Library.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void DownloadAndBlur()
{
  var url = "https://...jpg";
  var fileName = Path.GetFileName(url);
  DownloadImage(url).ContinueWith(task1 =>
  {
    var originalImageBytes = task1.Result;
    var originalImagePath = Path.Combine(ImageResourcesPath, fileName);
    SaveImage(originalImageBytes, originalImagePath).ContinueWith(task2 =>
    {
      BlurImage(originalImagePath).ContinueWith(task3 =>
      {
        var blurredImageBytes = task3.Result;
        var blurredFileName = $"{Path.GetFileNameWithoutExtension(fileName)}_blurred.jpg";
        var blurredImagePath = Path.Combine(ImageResourcesPath, blurredFileName);
        SaveImage(blurredImageBytes, blurredImagePath).ContinueWith(task4 =>
        {
          done = true;
        });
      });
    });
  });
}
csharp

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.

Asynchronous Programming with Async and Await

Let's rewrite the DownloadAndBlur method, but this time with async and await.

1
2
3
4
5
6
7
8
9
10
11
12
13
static async void DownloadAndBlur()
{
  var url = "https://...jpg";
  var fileName = Path.GetFileName(url);
  var originalImageBytes = await DownloadImage(url);
  var originalImagePath = Path.Combine(ImageResourcesPath, fileName);
  await SaveImage(originalImageBytes, originalImagePath);
  var blurredImageBytes = await BlurImage(originalImagePath);
  var blurredFileName = $"{Path.GetFileNameWithoutExtension(fileName)}_blurred.jpg";
  var blurredImagePath = Path.Combine(ImageResourcesPath, blurredFileName);
  await SaveImage(blurredImageBytes, blurredImagePath);
  done = true;
}
csharp

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?

The Purpose of the Async Keyword

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:

1
public 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!

Advantages of Async/Await

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.

Trade-offs and Conclusion

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.

55