.NET Backend

Why do we need async keyword in C#?

Last Tuesday, I had a talk in Wrocław about async/await in C# (thanks once again, you rock!). At some point, I asked the audience seemingly simple question. Why do we need async keyword at all? When I look at the folks, lots of them looked very confused since the answer seemed very obvious. Well, as you probably expect, it’s a little bit more complicated.

 

 

Async makes my method asynchronous…

At the very beginning, I should explain why the answer to my question seems trivial. If you would ask developers what async keyword does, they would probably answer „it makes the method asynchronous”. That seems perfectly fine, right? If I want my method to be internal, I write:

 


internal void MyMethod()
{
}

 

If I want to make my method to be virtual I write:

 


virtual void MyMethod()
{
}

 

So, logically when I want my method to be asynchronous I should type async. But here’s the thing. async doesn’t do that at all. It’s very simple to create a synchronous method with the async keyword in its declaration. The example below presents it:

 


async void MyAsyncMethod()
{
}

 

Even if you don’t have Resharper, you can still see the message in Visual Studio which informs about that:

 

 

Well, of course, some of you could think that the above method will run synchronously because there’s nothing inside. How about this?

 


        async Task TestAsync()
        {
            Task.Delay(100);
            Task.Delay(200);
            Task.Delay(300);
        }

 

And once again you can see the warning:

 

 

This applies to every method which doesn’t have await operator (as the error message says) inside. So, remember this simple rule:

no await operator == synchronous execution

Frankly, this should not surprise those of you who know how the async/await is actually compiled to the Intermediate Language.

 

Why await operator determines asynchrony?

Before we move further I want to make sure that you can answer the below questions:

  1. How are methods with async/await executed?
  2. What compiler generates instead of async/await?
  3. How is the async/await method chopped into smaller pieces?

If you don’t know the question for all the above, I encourage you to read my article about async/await which I wrote a year ago. Trust me, that understanding the basic concept will be crucial later in this paragraph.

Assuming that you have the knowledge, let’s ask a simple question. How should we interpret the await operator in our code? The very general answer may be that await represents the place in the code where we need the particular task to be completed, in order to continue execution. It could be some JSON from a web service, text from a file or anything else. We need something until we can move further. So let’s turn the question. How should we interpret lack of the await operator (even if something can be awaitable)? Let’s take a look once again at the example from the previous paragraph:

 


        async Task TestAsync()
        {
            Task.Delay(100);
            Task.Delay(200);
            Task.Delay(300);
        }

 

Here we don’t care about the completion of each Task.Delay. We call each line like fire and forget. Do something, but I don’t care when will you finish and what will you return. Therefore, if we don’t need the result of each Task, will there ever be a situation that we will need to suspend the execution and return the control to the caller? No. So, does it look like a synchronous execution? Exactly!
However, in order not to leave any doubt, let’s see what IL will be generated for the above method:

 

 

As you see, the MoveNext method inside the state machine has only one (implicit) state, since there’s no chance to suspend the method and move everything from the stack to the heap. So once again:

no await operator == one state in the MoveNext method == synchronous execution

 

So why do we need async keyword?

At this point, for some of you, this might be confusing. If the asynchrony is determined by the await operator, why the hell we need the async keyword? Why couldn’t the compiler be designed to treat the below code as async?

 


public Task<string> MyAsyncMethod()
    => await GetTextAsync();

 

Actually, the answer is very simple. Backwards compatibility. We need to remember that async/await doesn’t exist from the beginning of C# language. It was added in version 5.0, so before that, there was nothing special about these two words. Knowing this, consider the following code:

 


var await = 2;

 

The above code is perfectly legal in C#. You can create a variable named await. So, what would happen if the language designers decided that starting from the C# 5.0 every await would refer to the unary operator, not to the variable? They would break the backward compatibility because for sure someone in the past had named the variable await. Of course, we could expect that await would be interpreted differently, depending on the context, but that wouldn’t be a good idea because the compiler (despite being clever) cannot guess.
So instead of doing that, the language designers introduced also async keyword which eliminated this uncertainty. If your method is marked as async, you are not allowed to create a variable named await. Simple is that. This was a right solution for this particular feature.
All the existing code could still work because await referred to the variables (since there was no async keyword).
In new code, if you wanted to use await operator, you had to mark the method as async.

  • Pingback: dotnetomaniak.pl()

  • Adam Furmanek

    „They would break the backward compatibility because for sure someone in the past had named the variable await” — this is very silly example because detecting whether await is a name variable or a keyword is easy, this is not even close to ambiguity handled by C++ compilers. Async is required for backwards compatibility but not because of variable names.

    • Your’re correct Adam. My intention was to introduce await variable as an example but for some reason I sticked to that for the rest of the paragraph. Of course there are way more confusing scenarios when determing whether await refers to unary operator or something else, would not be that easy. The example here could be also a method called await, so having the below code:

      var result = await(something);

      it’d be way harder to determin whether we should desugar this into something.GetAwaiter() (in IL) or we should simply invoke this await thing as a function. I guess, I should extend the last paragraph a little bit.

      Anyway, thx for your reply!