C# Async Await Revisited

For those of you who have been using C# for some time now, you’ve no doubt come across the use of the Async and Await keywords for asynchronous programming and probably made extensive use of them yourselves. If you’re new to the language, asynchronous programming in the language—today—finds these keywords at its core. They make writing asynchronous methods very easy; those methods look very similar to their synchronous counterparts, and are almost as easy to read.

Note: This article is part of a common theme of articles related to asynchronous programming in C#, which will be followed with a look at UI-Thread management in a XAML application.

Setting Up Your Lab

If you would like to create the program along with this article, I’m using LINQPad 5, which you can download via this link.

I’ll be using LINQPad as later on in this article. For now, I’d like to show you something you need to be aware of when using Async Await. But if you want to do this in a standard .NET console application, you’ll be able to follow along with the code in the article just fine; just remember to make your methods static.

However, if you do want to try LINQPad 5—which is something I recommend if you’ve never used it before—here are a few instructions you’ll find useful.

When you open LINQPad, you’ll see something like this…

Async1
Figure 1: LINQPad 5

Make sure the ‘Language’ tab has C# Program selected, which will auto generate for you the Main method. Now, you’ll also need to add a reference to System.Threading.Tasks, which you’ll need towards the end of the article. To do this, press F4, which should open the Query properties window. With the Addition References tab selected, click Add. And, the assembly you need to add is…

mscorlib.dll

This assembly contains the namespace System.Threading.Tasks. And, to select this namespace so it’s usable in our application, select the Additional Namespace Imports tab, and click Pick from assemblies in the top right corner of the window. Then, in the namespaces list, select System.Threading.Tasks and click Add Selected Namespaces. Click Okay, and you’re ready to go.

The Keywords

Before we go into some code, let’s quickly look at the definitions of each of the keywords.

Async: You use the async modifier to specify that a method is asynchronous. Convention would now call this method an ‘async method’.

Await: The await operator is applied to a task (on-going work) in an async method, which suspends execution of that async method until the task completes. The await keyword can only be used in a method that has been modified with the async keyword.

Note: I’ve simplified the definitions a little for the sake of this article. You can find their official definition on msdn.microsoft.com.

Your First Asynchronous Method

Now we have that out of the way, let’s jump into the code. And, that code is going to be standard synchronous code, which looks something like this…

void Main()
{
   Loop();
   Console.WriteLine("End of line...");
}

void Loop()
{
   for(int i = 0; i < 100; i++)
   {
      Console.WriteLine(i);
   }
}

The output of this code is very simple. You’ll have the numbers 0 to 99 output in your results window, or console window if you aren’t using LINQpad. And, at the end, you’ll find the output ‘End of line…’. Now, what if that for-loop were doing some operation which meant it was going to take several seconds to complete?

While the program was waiting for this call to complete, the main program thread would be blocked, unable to do anything else until the for-loop was finished. If your application was a desktop application, the application would be clearly frozen, and the user may believe it had crashed; but, in fact, it is only waiting for the loop completion. This is a problem many of us have dealt with as developers, and as users, we’ve probably seen it more.

Step in Asynchronous Programming with Async Await…

Let’s re-write the code to do the work in an async fashion. The first thing we’ll need is a way to enter the asynchronous stack. In a desktop application, that way would normally be an event handler, which is executed at the press of a button. However, for this application, we’ll simulate that with the following code:

void Main()
{
   ThisWouldNormallyBeAnEventHandler();
   Console.WriteLine("End of line...");
}

async void ThisWouldNormallyBeAnEventHandler()
{
   for(int i = 0; i < 100; i++)
   {
      Console.WriteLine(i);
      await Task.Delay(1);
   }
}

As a rule of thumb, async voids are for event handlers or high-level methods that allow access to the asynchronous stack. Async voids can be referred to as ‘Fire and Forget’, and if the void method is not an event handler, you may want suffix the name with ‘FireAndForget’. Given that a console application doesn’t allow the main method to be async, we’ll use the async void method to enter the asynchronous stack.

It’s always worth keeping in mind that async voids come with some pitfalls, which we’ll cover when we talk about an async method with the return type of Task.

Now, run the program and observe the results. If you are using LINQPad, you will see something like this…

Async2
Figure 2: The Async Program Running

If you are using a console application, it is highly probable you saw the 0, then ‘End of line…’. After that, your program would have exited.

What is happening now is the code in the main method is continuing to execute after the loop method has been invoked, and it is not waiting for the method to return. Therefore, the method call is now a non-blocking call. And, program execution continues asynchronously along with the long running work being executed from the async method.

Now, give this a try and remove this line…

await Task.Delay(1);

…and observe the results. When removing this line, which contains the await keyword, this method—even though it’s been marked async—is now running synchronously. Under normal circumstances, IntelliSense would have underlined the method name and alerted you. That said, the take away here is that an async method without an await keyword is executed as if it were a synchronous method.

We’ll talk about Task.Delay() soon.

Return Type of Task ‘the Awaitable’

You’ve now looked at the async method with the return type of void, which is fire and forget. Let’s now look at what you can do if you wanted to return something else and not had the method be fire and forget. You can’t do something like this…

async bool IsRunning(){}

…because an async method must have a return type of void, Task, or Task<T>. We’ll look at Task first, and here’s a little bit of code.

void Main()
{
   ThisWouldNormallyBeAnEventHandler();
   Console.WriteLine("End of line...");
}

async void ThisWouldNormallyBeAnEventHandler()
{
   await LoopAsync();
}

async Task LoopAsync()
{
   for(int i = 0; i < 100; i++)
   {
      Console.WriteLine(i);
      await Task.Delay(1);
   }
}

Unlike the async void, our LoopAsync() method is awaitable; that is, you can apply the await keyword at the point of calling the method. This allows the ThisWouldNormallyBeAnEventHandler() method to suspend execution until our LoopAsync method has completed. At the same time, the main program method is allowed to continue execution as it did in our last example. Also note that the method name Loop has been suffixed with ‘Async’, which is a widely used convention on a method that is awaitable and truly async. For the sake of this article, our method is only asynchronous because of the await on Task.Delay. In a real application, it could be downloading a file from an Internet source, and you wanted the application to continue execution while it downloaded.

As promised earlier that I would explain Task.Delay(). What it is, is a very elegant alternative to Thread.Sleep. It is non-blocking and, for the duration you’ve specified, the thread it is called on is allowed to continue executing other work. When the duration you specified expires, it will return to the point in the code it was called. This is unlike Thread.Sleep, which will suspend all execution on the thread in which it is called.

Quickly moving on to Task<T>, if you wanted your LoopAsync() to return some type such as a bool, you can do this…

async Task<bool> LoopAsync()

Where a standard return true; would work. However, this would work only if the method had been modified with the async keyword. There is a lot more to Task then we’ve covered in this article, which is enough to cover an article all on its own.

That said, and before we move on to something else, I want to quickly go through these points with you. Async Tasks are the bread and butter of asynchronous code and preferred over async voids. Async voids give you access to the stack by using them via event handlers. However, async voids have different error handling semantics when compared to async Task. Exceptions from an async void cannot be caught by a catch statement; instead, they are raised on the synchronization context, which is a term we have not yet covered in this article. The topic of the synchronization context, like Task, is an article in itself. But, in a nutshell, it is one of a number of mechanisms under the hood of async await that allow it all to work.

Intermediate Language

I mentioned earlier I was using LINQPad because I wanted to show you something in regards to async await. And that something is the amount of IL generated when using the keywords. These keywords are powerful tools in your box, and are there to be used. However, there is a cost. And, that cost is the potential overhead of the amount of IL produced when using them. Let’s take this little piece of code, for example …

void Main()
{
   Write();
}

void Write()
{
   Console.WriteLine("Hey There!");
}

Run this program, and select the IL tab. You’ll see something like this…

Async3
Figure 3: Generated IL Code

Now, add the async modifier to the Write method, run the program again, and take a look at the IL produced. It’s quite considerable, considering the size of the program.

Food for thought.

As usual, you can find me on Twitter as @GLanata if you have any questions.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read