Во что разворачиваются async и await в C#

Ключевые слова async и await в C# скрывают под собой сложную механику преобразования метода в конечный код на IL. Компилятор превращает асинхронные методы в конечный автомат, управляемый структурой AsyncStateMachine.

Развертывание async-метода

При компиляции асинхронный метод превращается в конечный автомат, который управляет состоянием выполнения. Рассмотрим пример:

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

Компилятор развернет этот метод в класс, реализующий IAsyncStateMachine. Он будет выглядеть примерно так (упрощенно):

private struct GetNumberAsyncStateMachine : IAsyncStateMachine
{
    public int State;
    public AsyncTaskMethodBuilder<int> Builder;
    private TaskAwaiter Awaiter;

    public void MoveNext()
    {
        try
        {
            if (State == -1)
            {
                Awaiter = Task.Delay(1000).GetAwaiter();
                if (!Awaiter.IsCompleted)
                {
                    State = 0;
                    Builder.AwaitOnCompleted(ref Awaiter, ref this);
                    return;
                }
            }
            Awaiter.GetResult();
            Builder.SetResult(42);
        }
        catch (Exception ex)
        {
            Builder.SetException(ex);
        }
    }

    public void SetStateMachine(IAsyncStateMachine stateMachine) { }
}

Что делает await

Ключевое слово await заставляет компилятор развернуть асинхронный метод в IAsyncStateMachine, который управляет выполнением. await создает TaskAwaiter, который отслеживает состояние задачи.

Например, код:

await Task.Delay(1000);

Будет превращен в:

TaskAwaiter awaiter = Task.Delay(1000).GetAwaiter();
if (!awaiter.IsCompleted)
{
    State = 0;
    Builder.AwaitOnCompleted(ref awaiter, ref this);
    return;
}

awaiter.GetResult();

Это позволяет задаче выполняться асинхронно без блокировки потока.

AsyncStateMachine и стек вызовов

Так как асинхронные методы используют AsyncStateMachine, их стек вызовов не сохраняется в стандартном виде. Вместо этого используется MoveNext(), который управляет состояниями выполнения. Это также объясняет, почему исключения в async void методах трудно перехватывать.


Во время компиляции async-методы превращаются в конечные автоматы, управляемые AsyncStateMachine. await использует TaskAwaiter для управления состоянием выполнения. Этот механизм позволяет избежать блокировки потоков и эффективно работать с асинхронным кодом.