Что происходит под капотом при использовании async/await в C#
Что проверяют
Понимаете ли вы, во что компилятор превращает async-метод и где на самом деле выполняется продолжение после await. От этого зависят практические вопросы, которые идут следом: откуда берутся дедлоки на .Result, зачем ConfigureAwait(false) и почему опасен async void.
Как отвечать
Компилятор переписывает async-метод в конечный автомат:
- state machine: сгенерированный тип, реализующий
IAsyncStateMachine(в Release это структура, в Debug класс). Локальные переменные становятся его полями, код разрезается по точкамawaitна участки внутриMoveNext(), а поле состояния помнит, откуда продолжить; - builder:
AsyncTaskMethodBuilder<T>создаёт возвращаемыйTaskи выставляет в него результат или исключение; - awaiter:
awaitработает с любым типом, у которого естьGetAwaiter()сIsCompleted,OnCompletedиGetResult().
На await автомат сначала проверяет IsCompleted. Если операция уже завершена, выполнение идёт дальше синхронно, и автомат даже не попадает в кучу. Если нет, он переносится в кучу, регистрирует MoveNext как продолжение и возвращает управление вызывающему. Поток при этом свободен: во время сетевого запроса никакой поток не ждёт, ожидание обслуживает ОС через IOCP или epoll.
Где выполнится продолжение: по умолчанию захватывается SynchronizationContext, а если его нет, текущий TaskScheduler. В WPF и WinForms это UI-поток, в ASP.NET Core контекста нет, и продолжение уходит в пул потоков. ConfigureAwait(false) отключает захват, это стандарт для библиотечного кода.
Исключение сохраняется в Task и пробрасывается на await с исходным стеком через ExceptionDispatchInfo.
Где спотыкаются
Говорят, что async запускает код в новом потоке. Метод выполняется синхронно до первого незавершённого await, а Task.Run это отдельная история.
Блокируют асинхронный код через .Result или .Wait() там, где есть контекст синхронизации. Продолжение ждёт занятый UI-поток, а тот ждёт задачу: классический дедлок.
Пишут async void вне обработчиков событий. Такой метод нельзя дождаться, а его исключение уходит в контекст синхронизации и роняет процесс.