Что на самом деле происходит с вашим async-методом: компилятор C# переписывает его в конечный автомат
Meziantou разобрал, во что превращается async-метод после компиляции. Сам метод становится заглушкой: она создаёт структуру-автомат, заводит билдер вроде AsyncTaskMethodBuilder, копирует параметры в поля, ставит состояние -1, вызывает Start и возвращает Task. Вся логика переезжает в MoveNext.
В Release автомат это struct, в Debug это class. Пока метод не засыпает, автомат живёт на стеке. Как только он приостанавливается, билдер упаковывает его в кучу. Поле state хранит точку возобновления: -1 значит выполняется, -2 завершён, 0 и выше номер await. В поля уходят только параметры и локальные переменные, которые нужны после await, остальные остаются локальными. Слоты под awaiter выделяются на тип, а не на каждый await, поэтому несколько await на одном типе делят одно поле.
Сам await превращается в switch по полю плюс регистрацию продолжения. Если awaiter уже завершён, код идёт дальше без остановки. Если нет, сохраняется состояние и awaiter, вызывается AwaitUnsafeOnCompleted, и MoveNext возвращается. Исключения ловятся сгенерированным try/catch и уходят в SetException, поэтому всплывают на await, а не в месте вызова. До первого незавершённого await метод выполняется синхронно.
Бенчмарки автора показывают цену этого механизма: синхронный цикл 2,7 нс, async без реального ожидания 47,9 нс, async с приостановкой на каждой итерации 8820 нс. Разница в сотни раз в горячих циклах заметна, так что await на каждом элементе лучше пересобирать. Мерить стоит только в Release, иначе картина искажается.
Источник: https://www.meziantou.net/how-the-csharp-compiler-rewrites-an-async-method.htm
Post #1906
937