Ответ
Ключевое слово await компилируется в конечный автомат (state machine), реализованный компилятором как структура, реализующая интерфейс IAsyncStateMachine. Этот автомат разбивает асинхронный метод на части и управляет возобновлением выполнения после завершения асинхронной операции.
Как это работает:
- Компилятор создаёт приватную вложенную структуру (
<MethodName>d__X), которая хранит:- Состояние метода (
state, обычно-1,0,1,2...). - Локальные переменные исходного метода (превращаются в поля структуры).
TaskAwaiter(или другойawaiter) для ожидания задачи.AsyncTaskMethodBuilderдля управления созданием и завершением возвращаемойTask.
- Состояние метода (
- Вся логика метода перемещается в метод
MoveNext()этой структуры. - При встрече
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).
Ответ 18+ 🔞
А, ну это же классика, про которую каждый второй думает, что там магия какая-то, а на деле — обычный конечный автомат, который компилятор из тебя вытворяет. Представь себе, ты написал красивый async метод, а компилятор смотрит на это и такой: «О, блядь, опять разгребать...» — и начинает генерировать тебе под капотом структуру, которая всё это хозяйство и крутит.
Вот смотри, ключевое слово await — это не волшебная палочка. Это команда для компилятора: «Слушай, а сделай-ка тут точку остановки, подпишись на завершение задачи, а потом, когда она сделается, продолжи отсюда, да не забудь контекст восстановить, а то я тут локальные переменные использую, ёпта!».
И компилятор, сука, послушно создаёт эту самую структуру, которая реализует IAsyncStateMachine. Название-то какое, блядь, серьёзное — «Машина состояний». А по сути — просто переключатель state, который говорит: «Мы сейчас на шаге 0, ждём первую задачу» или «Мы на шаге 1, ждём вторую».
Как этот цирк работает:
- Твой метод компилятор разрывает на куски, как тупую курицу. Всё, что до первого
await— один кусок. Всё, что после него — другой. И так далее. - Все твои милые локальные переменные —
int result— превращаются в поля этой сгенерированной структуры. Потому что между вызовамиMoveNext()их же где-то хранить надо, а в стеке они не живут вечно. - Когда ты делаешь
await FetchFromNetworkAsync(), компилятор не тупо ждёт. Он сначала спрашивает: «А задача-то уже может и готова?» (IsCompleted). Если готова — ну, ебать, красота, просто забираем результат и идём дальше, синхронно, без всяких подписок. - А если не готова — вот тут начинается настоящий пиздец. Состояние (
state) выставляется в «1», чтобы при возобновлении понять, откуда продолжать. Потом через этого самого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; // -1, 0, 1, 2... Где мы, блядь, находимся?
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; // Запомним, что надо будет продолжить с шага 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); // Если что-то пошло не так — пробрасываем исключение в таску
return;
}
builder.SetResult(finalResult); // Всё, пизда, таска успешно завершена с результатом
}
}
А зачем это всё, спрашивается, понимать? Да затем, чувак!
- Накладные расходы: Теперь ты видишь, что создаётся объект (структура, которую всё равно боксят), поля,
awaiter. Это не бесплатно. Для горячего цикла — может быть важно. ConfigureAwait(false): Ты теперь понимаешь, откуда ноги растут у этого совета. Этот самыйbuilder.AwaitUnsafeOnCompletedможет захватывать контекст синхронизации (например, контекст UI-потока), чтобы потом в нём же и возобновиться. Если ты пишешь библиотечный код, которому похуй на контекст,ConfigureAwait(false)говорит компилятору: «Не захватывай контекст, возобновляйся где попало». Это может здорово сэкономить производительность и избежать дедлоков.- Не потоки: Это самое главное. Здесь нет никаких дополнительных потоков! Это модель на основе продолжений (continuations). Поток освобождается, когда ждёт I/O, а потом, когда I/O готов, продолжение (твой
MoveNext) может быть выполнено на любом свободном потоке из пула (или на том же самом, если контекст был захвачен). Вся магия — в умной подписке и колбэках, а не в создании тысяч потоков.
Короче, async/await — это не магия, а просто очень, блядь, умная и удобная синтаксическая надстройка над старыми-добрыми задачами и колбэками. Компилятор — тот ещё извращенец, который превращает твой линейный код в машину состояний. Но когда понимаешь, как оно работает, сразу меньше страха и больше уважения к технологии. И меньше шансов накосячить с дедлоками, потому что ты уже представляешь, что там под капотом творится.