Under the hood of 'async await' in .NET

Apr 5, 2025

In .NET, async and await are syntactic sugar for a state machine that the C# compiler generates under the hood. Here’s how it works:

1. Compiler Transformation into a State Machine

When you mark a method as async, the C# compiler transforms it into a state machine, allowing it to handle asynchronous execution seamlessly.

For example, consider this simple asynchronous method:

public async Task<int> FetchDataAsync()
{
    await Task.Delay(1000);
    return 42;
}

The compiler transforms it into a state machine structure, roughly equivalent to:

public Task<int> FetchDataAsync()
{
    var stateMachine = new FetchDataAsyncStateMachine();
    stateMachine.MoveNext(); // Begin execution
    return stateMachine.Task;
}

2. The State Machine Class

The compiler generates a struct implementing IAsyncStateMachine. This struct contains:

  • State tracking variable (e.g., _state)

  • A builder object (AsyncTaskMethodBuilder<T>), which helps in executing the method

  • Fields to store local variables and awaitables

  • The MoveNext() method, which drives the execution

3. The MoveNext() Method

The MoveNext() method is where the logic happens. It works like a switch statement that jumps between states based on await points.

For example:

void MoveNext()
{
    try
    {
        if (_state == -1)  // Initial state
        {
            _taskAwaiter = Task.Delay(1000).GetAwaiter();
            if (!_taskAwaiter.IsCompleted)
            {
                _state = 0;  // Store state
                _builder.AwaitOnCompleted(ref _taskAwaiter, ref this);
                return;
            }
        }

        // Resume execution after await
        int result = 42;
        _builder.SetResult(result);
    }
    catch (Exception ex)
    {
        _builder.SetException(ex);
    }
}

4. Role of Task, TaskAwaiter, and Builder

  • Task.Delay(1000).GetAwaiter() returns a TaskAwaiter, which helps in checking if the task is completed.

  • If not completed, execution is paused, and the continuation is scheduled.

  • _builder.SetResult(42); completes the task with the result.

5. Continuations and Threading

  • The MoveNext() method gets scheduled to run on the captured SynchronizationContext (like UI thread for WPF, ASP.NET context, or thread pool).

  • If the context doesn't matter, ConfigureAwait(false) can be used to avoid overhead.

Summary

  • async/await is converted into a state machine.

  • The method is split into multiple states.

  • The compiler generates MoveNext() to handle execution flow.

  • Tasks and awaiters manage asynchronous execution.

  • Continuations are scheduled based on context.

Vitalii Lakomov