Во что компилируется ключевое слово await в C#?

«Во что компилируется ключевое слово await в C#?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Ключевое слово await компилируется в конечный автомат (state machine), реализованный компилятором как структура, реализующая интерфейс IAsyncStateMachine. Этот автомат разбивает асинхронный метод на части и управляет возобновлением выполнения после завершения асинхронной операции.

Как это работает:

  1. Компилятор создаёт приватную вложенную структуру (<MethodName>d__X), которая хранит:
    • Состояние метода (state, обычно -1, 0, 1, 2...).
    • Локальные переменные исходного метода (превращаются в поля структуры).
    • TaskAwaiter (или другой awaiter) для ожидания задачи.
    • AsyncTaskMethodBuilder для управления созданием и завершением возвращаемой Task.
  2. Вся логика метода перемещается в метод MoveNext() этой структуры.
  3. При встрече await:
    • Если задача уже завершена (IsCompleted == true), выполнение продолжается синхронно.
    • Если задача не завершена, состояние (state) сохраняется, метод подписывается на уведомление о завершении задачи через builder.AwaitUnsafeOnCompleted(...), и управление возвращается вызывающему коду.
    • При завершении задачи вызывается MoveNext() снова, восстанавливается контекст и выполняется код после await.

Пример преобразования: Исходный код:

public async Task<int> GetDataAsync()
{
    Console.WriteLine("Start");
    int result = await FetchFromNetworkAsync();
    Console.WriteLine("Result: " + result);
    return result + 1;
}

Упрощённый вид сгенерированного кода (концептуально):

private struct <GetDataAsync>d__0 : IAsyncStateMachine
{
    public int state;
    public AsyncTaskMethodBuilder<int> builder;
    private int result; // Локальная переменная стала полем
    private TaskAwaiter<int> awaiter;

    void MoveNext()
    {
        int finalResult;
        try
        {
            if (state == 0) // Первый вход в метод
            {
                Console.WriteLine("Start");
                awaiter = FetchFromNetworkAsync().GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    state = 1; // Устанавливаем состояние для точки возобновления
                    builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
                    return; // Выход, управление возвращается вызывающему
                }
            }
            // Сюда попадаем при возобновлении (state == 1) или если задача завершилась синхронно
            result = awaiter.GetResult(); // Получаем результат асинхронной операции
            Console.WriteLine("Result: " + result);
            finalResult = result + 1;
        }
        catch (Exception ex)
        {
            builder.SetException(ex); // Проброс исключения в возвращаемую Task
            return;
        }
        builder.SetResult(finalResult); // Успешное завершение Task
    }
}

Почему это важно понимать? Это объясняет накладные расходы на создание объекта состояния, важность использования ConfigureAwait(false) в библиотечном коде (чтобы избежать лишних захватов контекста синхронизации) и то, почему async/await — это не потоки, а модель на основе продолжений (continuations).