Skip to content

RuslanPr0g/AsyncAwaiDEEPDIVE

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

54 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Async and await

keywords came with C# 5 as a cool new feature for handling asynchronous tasks. They allow us to specify tasks to be executed asynchronously in an easy and straightforward fashion. However, some people are mystified by asynchronous programming and are not sure how it actually works. I will try to give you an insight of the magic that happens under the hood when async and await are used.

Awaiter Pattern

C# language compiles some of the features as a syntactic sugar, which means that certain language features are just conveniences that translate into existing language features. A lot of those syntactic sugar features expand into patterns. Those patterns are based on method calls, property lookups or interface implementations. await expression is one of those syntactic sugar features. It leverages a pattern based on a few method calls. In order for a type to be awaitable, it has to meet the following requirements:

  • It has to have the following method: INotifyCompletion GetAwaiter()
  • Besides implementing the INotifyCompletion interface, the return type of the GetAwaiter method has to have the following: IsCompleted property of type bool, GetResult() method which returns void If you take a look at Task class, you will see that it meets all the above requirements.

    So, a type doesn’t even need to implement some specific interface in order to be awaitable. It just has to have a method with a specific signature. It is similar to duck typing.

    If it walks like a duck and it quacks like a duck, then it must be a duck.

    In this case it is

    If it has certain methods with certain signatures, then it has to be awaitable.

    To give you an illustrative example of this, I will create some custom class and make it awaitable. So, here is my class:

public class MyAwaitableClass
{

}

When I try to await an object of MyAwaitableClass type, I get the following error:

image


It says: 'MyAwaitableClass' does not contain a definition for 'GetAwaiter' and no extension method 'GetAwaiter' accepting a first argument of type 'MyAwaitableClass' could be found (are you missing a using directive or an assembly reference?)

Let’s add GetAwaiter method to our class:


public class MyAwaitableClass
{
    public MyAwaiter GetAwaiter()
    {
        return new MyAwaiter();
    }
}

public class MyAwaiter
{
    public bool IsCompleted
    {
        get { return false; }
    }
}


We can see that the compiler error changed:

image


Now it says: 'MyAwaiter' does not implement 'INotifyCompletion'

Ok, let’s create implement the INotifyCompletion interface in MyAwaiter:


public class MyAwaiter : INotifyCompletion
{
    public bool IsCompleted
    {
        get { return false; }
    }

    public void OnCompleted(Action continuation)
    {
    }
}


and see what the compiler error looks like now:

image


It says: ‘MyAwaiter’ does not contain a definition for ‘GetResult’

So, we add a GetResult method and now we have the following:


public class MyAwaitableClass
{
    public MyAwaiter GetAwaiter()
    {
        return new MyAwaiter();
    }
}

public class MyAwaiter : INotifyCompletion
{
    public void GetResult()
    {
    }

    public bool IsCompleted
    {
        get { return false; }
    }

    //From INotifyCompletion
    public void OnCompleted(Action continuation)
    {
    }
}


And we can also see that there are no compiler errors,

image


which means we have made an awaitable type.

Now that we know which pattern does the await expression leverage, we can take a look under the hood to see what actually happens when we use async and await.

Async

For every async method a state machine is generated. This state machine is a struct that implements IAsyncStateMachine interface from System.Runtime.CompilerServices namespace. This interface is intended for compiler use only and has the following methods:

  • MoveNext() - Moves the state machine to its next state.
  • SetStateMachine(IAsyncStateMachine) - Configures the state machine with a heap-allocated replica. Now let’s take a look at the following code:

class Program
{
    static void Main(string[] args)
    {
    }

    static async Task FooAsync()
    {
        Console.WriteLine("Async method that doesn't have await");
    }
}


We have an async method named FooAsync. You may notice that it lacks await operator, but I left it out for now for the sake of simplicity.
Now let’s take a look at the compiler generated code for this method. I am using dotPeek to decompile the containing .dll file. To see what is going on behind the scenes, you need to enable Show Compiler-generated Code option in dotPeek.
Compiler generated classes usually contain < and > in their names which are not valid C# identifiers so they don’t conflict with user-created artifacts.

Let’s take a look what compiler generated for our FooAsync method:

image


Our Program class contains Main and FooAsync methods as expected, but we can also see that compiler generated a struct called Program.d__1. That struct is a state machine that implements the IAsyncStateMachine interface. Besides the IAsyncStateMachine interface methods, this struct also has the following fields:

<>1__state which indicates the current state of the state machine

<>t__builder of type AsyncTaskMethodBuilder which is used for creation of asynchronous methods and returning the resulting task. The AsyncTaskMethodBuilder struct is also intended for use by the compiler.


We will see the code of this struct in more detail, but first let’s take a look at what compiler-generated FooAsync method looks like after we decompiled it:


private static Task FooAsync()
{
  Program.d__1 stateMachine;
  stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
  stateMachine.<>1__state = -1;
  stateMachine.<>t__builder.Startd__1>(ref stateMachine);
  return stateMachine.<>t__builder.Task;
}


This is what compiler transforms async methods to. The code inside the method does the following:

Instantiate the method’s state machine

Create new AsyncTaskMethodBuilder and set it as state machine’s builder

Set the state machine to a starting state

Start the builder with the method’s state machine by calling the Start method.

Return the Task


As you can notice, compiler-generated FooAsync method doesn’t contain any of the code our original FooAsync method had. That code represented the functionality of the method. So where is that code? That code is moved to state machine’s MoveNext method. Let’s take a look at Program.d_1 struct now:

image


The MoveNext method contains method’s code inside of a try block. If some exception occurs in our code, it will be given to the method builder which propagates it all the way to the task. After that, it uses method builder’s SetResult method to indicate that the task is completed.
Now we saw how async methods look under the hood. For the sake of simplicity, I didn’t put any await inside of the FooAsync method, so our state machine didn’t have a lot of state transitions. It just executed our method and went to a completed state, i.e. our method executed synchronously. Now it is time to see how MoveNext method looks like when a method awaits some task inside of its body.

Await

Let’s take a look at the following method:

image


It awaits some QuxAsync method and uses its task result.
If we decompile it using dotPeek, we will notice that the compiler generated method has the same structure as FooAsync even if the original methods are different:

image


What makes the difference is the state machine’s MoveNext method. Now that we have an await expression inside of our method, the state machine loks like this:


[CompilerGenerated]
[StructLayout(LayoutKind.Auto)]
private struct d__2 : IAsyncStateMachine
{
  public int <>1__state;
  public AsyncTaskMethodBuilder <>t__builder;
  private TaskAwaiter <>u__1;

  void IAsyncStateMachine.MoveNext()
  {
	int num1 = this.<>1__state;
	try
	{
	  TaskAwaiter awaiter;
	  int num2;
	  if (num1 != 0)
	  {
		Console.WriteLine("This happens before await");
		awaiter = Program.QuxAsync().GetAwaiter();
		if (!awaiter.IsCompleted)
		{
		  this.<>1__state = num2 = 0;
		  this.<>u__1 = awaiter;
		  this.<>t__builder.AwaitUnsafeOnCompleted, Program.d__2>(ref awaiter, ref this);
		  return;
		}
	  }
	  else
	  {
		awaiter = this.<>u__1;
		this.<>u__1 = new TaskAwaiter();
		this.<>1__state = num2 = -1;
	  }
	  Console.WriteLine("This happens after await. The result of await is " + (object) awaiter.GetResult());
	}
	catch (Exception ex)
	{
	  this.<>1__state = -2;
	  this.<>t__builder.SetException(ex);
	  return;
	}
	this.<>1__state = -2;
	this.<>t__builder.SetResult();
  }

  [DebuggerHidden]
  void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
  {
	this.<>t__builder.SetStateMachine(stateMachine);
  }
}


The following image contains an explanation of the above state machine:

image


So, what await actually does is the following:

image


Every time you create an async method, the compiler generates a state machine for it. Then for each await inside of that method, it does the following:
Executes the method to the await expression
Checks if the method being awaited has already completed: [If yes, executes the rest of the method] | [If no, uses callback to execute the rest of the method when the method being awaited completes]

About

Deep dive into async await

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages