Во что превращается метод, помеченный async в C#?
Что проверяют
Знаете ли вы, что происходит на уровне компилятора. Вопрос отделяет тех, кто пользуется async по образцу, от тех, кто понимает цену и поведение.
Как отвечать
Компилятор переписывает метод в конечный автомат. Тело разрезается на участки между точками await, и каждый участок становится шагом автомата. Генерируется структура (в релизной сборке именно структура, чтобы не выделять память, пока метод идёт синхронно) с полями под локальные переменные, номером текущего состояния и методом MoveNext, который выполняет очередной шаг.
Дальше происходит следующее:
- вызов метода запускает
MoveNextсинхронно в текущем потоке; - на первом
awaitнад незавершённой задачей автомат сохраняет состояние, регистрирует продолжение черезAwaitUnsafeOnCompletedи возвращает вызывающемуTask; - когда ожидаемая операция завершится, планировщик вызывает
MoveNextснова, и метод продолжается с сохранённого места; - построением результата занимается builder (
AsyncTaskMethodBuilder), он же переносит исключение в задачу вместо выброса на месте.
Из этого вытекают практические следствия, которые стоит назвать: локальные переменные, живущие через await, становятся полями автомата и попадают в кучу; если все ожидания завершены, автомат отрабатывает целиком синхронно и Task может вернуться из кеша готовых значений; для горячих путей есть ValueTask, который избавляет от выделения памяти в синхронном случае; сам по себе async не создаёт ни потока, ни параллелизма.
Где спотыкаются
Считают, что async запускает метод в фоне.
Не знают, почему исключение из асинхронного метода всплывает только на await: оно упаковано в задачу.
Пишут async там, где метод просто возвращает чужую задачу: лишний автомат без пользы, достаточно вернуть Task напрямую.