The state machine in C# with async/await

7/22/2024
5 minute read

You often here that the async/await keywords leads to a state machine. But what does that mean? Let's discuss this with a simple example.

The state machine

Let's consider the following code:

async Task<Dto> GetAllAsync()
{
    using var client = new HttpClient();
    client.BaseAddress = new Uri("https://pokeapi.co/api/v2/pokemon/");
    var response = await client.GetAsync("");
    response.EnsureSuccessStatusCode();
    var content = await response.Content.ReadAsStringAsync();
    return JsonSerializer.Deserialize<Dto>(content);
}

public class Dto;

We have multiple await calls here - the first one to get the response and the second one to read the content. So what happens here?

Cutting the method along the await border

Let's start with what async / await is all about: We want to utilize ressources more efficiently by not blocking the caller while waiting for a result. So everytime we call await, we know (given that it is I/O bound at least) that we have no work to do but wait for the result. So wouldn't it be nice to basically go out of the method in that moment and return once the result is there? And that is exactly the idea.

The compiler will cut the method along the await border and create a state machine. So you can imagine it like the following (I will oversimplify here):

public class GetAllAsync_StateMachine
{
    public ContinuationMachine _builder = ContinuationMachineBuilder.Create();

    private int _state = 0;
    private HttpClient _client;
    private HttpResponseMessage _response;
    private string _content;

    private void MoveNext()
    {
        switch (_state)
        {
            case 0:
                _client = new HttpClient();
                _client.BaseAddress = new Uri("https://pokeapi.co/api/v2/pokemon/");
                _client.GetAsync("");
                _builder.Continue(ref this);
                break;
            case 1:
                _response.EnsureSuccessStatusCode();
                _response.Content.ReadAsStringAsync();
                _state = 2;
                _builder.Continue(ref this);
                break;
            case 2:
                return JsonSerializer.Deserialize<Dto>(_content);
        }
    }
}

This is a very simplified version of what the compiler does and leaves out vital parts like - how does the content be assigned from the HttpClient. The important part is that we synchrously call the bits between the await until the await and then use a mechanism via callbacks (that is why we use ref this) to continue the method. So once the HTTP call is done, we go back into the method and continue where we left off (state 1, and if that finishes then state 2 and so on).

TaskScheduler

The thing you put your continuation on is the TaskScheduler. The TaskScheduler takes your state machine and schedules it to be executed once the awaited task is done. So the TaskScheduler is responsible for the continuation. There are more nifty bits in here like SynchronizationContext and ThreadPools but they are details.

So small recap: When using async / await the compiler will cut the method along the await border and create a state machine. This state machine will be scheduled by the TaskScheduler to continue once the awaited task is done.

Where is the state machine stored?

That is very simple to answerIn general: Task or Task<T>. Yes that is where your stuff is stored. The current state (including the continuation) as well as the result of the awaited task. So the Task is the state machine.

But furthermore your Task object also stores exceptions. Exceptions flow a bit different with async / await than with synchronous code.

Exceptions with async / await

Imagine the following code:

static async Task ThrowExceptionAsync()
{
    await Task.Yield();
    throw new Exception("Hey");
}

The compiler does the following code out of it:

try
{
    YieldAwaitable.YieldAwaiter awaiter;
    // Here is some other stuff
    awaiter.GetResult();
    throw new Exception("Hey");
}
catch (Exception exception)
{
    <>1__state = -2;
    <>t__builder.SetException(exception);
}

Important is to note that we have zero throws inside the catch block. That means nothing will bubble up if we have an exception in the asynchrnous part of the method. The exception will be stored in the Task object itself.

And maybe now you can see why async void is a bad idea in general: Your exceptions will be lost. They are there but you cannot catch them or handle them in any way. The same applies to async Task if you don't await the Task itself. So something like _ = MyThrowingAsyncTask();.

static async Task ThrowExceptionAsync()
{
    throw new Exception("Hey");
    await SomethingAsync();
}

_ = ThrowExceptionAsync(); // Will cause an exception (but doesn't bubble)

So will:

static async void ThrowExceptionAsync()
{
    throw new Exception("Hey");
    await SomethingAsync();
}

ThrowExceptionAsync(); // Will cause an exception (but doesn't bubble)

But why does it bubble then?

We saw that an exception is catched and stored on the Task object without bubbling it up in any fashion. So why does it bubble up then or better: When does it throw then? The simple answer: When you await the Task object. The await call will be translated to something like GetAwaiter().GetResult() and that is where the exception is thrown from the Task object to you.

Upcoming .NET User Forum Zurich / 6th July 2021

I am looking forward to give a talk about some insights and pitfalls of async / await.

I will talk about the differences between asynchronous and parallel programming. Also a brief outlook how the state machine internally works. Feel free to join here: https://www.meetup.com/dotnet-zurich/events/278916769/

ASP.NET Core - Why async await is useful

Did you ever wonder why you "should" use async and await in your ASP.NET Core applications? Most probably, you heard something about performance. And there is some truth to it, but not in the way you might think.

So let's discuss this with smaller examples.

Async Await Pitfalls / Guidelines - .NET User Group Zurich Presentation

On 6th of July I had the honor to present some topics about async/await. Mainly:

  • What is asynchronous programming
  • Deadlocks and ConfigureAwait
  • How does the state machine work
  • Pitfalls and general Guidelines
  • ValueTask

You'll find all the slides and the whole talk in the blog.

An error has occurred. This application may no longer respond until reloaded. Reload x