Найти в Дзене
using Dev

Как на самом деле работает Async/Await в C# ч.6

async/ await под капотом Теперь, когда мы знаем, как мы сюда попали, давайте углубимся в то, как это на самом деле это работает. Для справки, вот еще раз наш пример синхронного метода: и еще раз вот как выглядит соответствующий метод с async/ await: Простор свежего воздуха по сравнению со всем, что мы видели до сих пор. Подпись изменилась с void на async Task, мы вызываем ReadAsync и WriteAsync вместо Read и Write соответственно, и обе эти операции имеют префикс await. Вот и все. Компилятор и основные библиотеки берут на себя все остальное, фундаментально меняя способ фактического выполнения кода. Давайте углубимся в то, как это сделать. Преобразование компилятора Как мы уже видели, как и в случае с итераторами, компилятор переписывает асинхронный метод в метод, основанный на конечном автомате. У нас все еще есть метод с той же сигнатурой, которую написал разработчик ( public Task CopyStreamToStreamAsync(Stream source, Stream destination)), но тело этого метода совершенно другое: Обр
Оглавление

async/ await под капотом

Теперь, когда мы знаем, как мы сюда попали, давайте углубимся в то, как это на самом деле это работает. Для справки, вот еще раз наш пример синхронного метода:

-2

и еще раз вот как выглядит соответствующий метод с async/ await:

-3

Простор свежего воздуха по сравнению со всем, что мы видели до сих пор. Подпись изменилась с void на async Task, мы вызываем ReadAsync и WriteAsync вместо Read и Write соответственно, и обе эти операции имеют префикс await. Вот и все. Компилятор и основные библиотеки берут на себя все остальное, фундаментально меняя способ фактического выполнения кода. Давайте углубимся в то, как это сделать.

Преобразование компилятора

Как мы уже видели, как и в случае с итераторами, компилятор переписывает асинхронный метод в метод, основанный на конечном автомате. У нас все еще есть метод с той же сигнатурой, которую написал разработчик ( public Task CopyStreamToStreamAsync(Stream source, Stream destination)), но тело этого метода совершенно другое:

-4

Обратите внимание, что единственное отличие подписи от того, что написал разработчик, — это отсутствие async самого ключевого слова. async н а самом деле не является частью сигнатуры метода; например unsafe, когда вы помещаете его в сигнатуру метода, вы выражаете детали реализации метода, а не что-то, что фактически раскрывается как часть контракта. Использование async/await для реализации Task метода - return является деталью реализации.

Компилятор сгенерировал структуру с именем <CopyStreamToStreamAsync>d__0 и инициализировал нулевым значением экземпляр этой структуры в стеке. Важно отметить, что если асинхронный метод завершается синхронно, этот конечный автомат никогда не покинет стек. Это означает, что с конечным автоматом не связано никаких выделений, если только метод не должен завершиться асинхронно, то есть await к этому моменту еще не завершен. Подробнее об этом чуть позже.

Эта структура представляет собой конечный автомат метода, содержащий не только всю преобразованную логику из того, что написал разработчик, но также поля для отслеживания текущей позиции в этом методе, а также все «локальное» состояние, которое компилятор извлек из. метод, который должен сохраняться между MoveNextвызовами. Это логический эквивалент реализации IEnumerable<T>/ IEnumerator<T>, которую мы видели в итераторе. (Обратите внимание, что код, который я показываю, взят из релизной сборки; в отладочных сборках компилятор C# фактически генерирует эти типы конечных автоматов как классы, поскольку это может помочь в определенных упражнениях по отладке).

После инициализации конечного автомата мы видим вызов AsyncTaskMethodBuilder.Create(). Хотя в настоящее время мы сосредоточены на Tasks, язык C# и компилятор позволяют возвращать произвольные типы ( «типы задач» async ) из методов, например, я могу написать метод public async MyTask CopyStreamToStreamAsync, и он будет нормально компилироваться, пока мы дополняем его. MyTaskо пределили ранее соответствующим образом. Эта целесообразность включает в себя объявление связанного типа «строитель» и связывание его с типом через AsyncMethodBuilder атрибут:

-5

В этом контексте такой «строитель» — это нечто, что знает, как создать экземпляр этого типа (свойства Task), завершить его либо успешно и с результатом, если это необходимо ( SetResult), либо с исключением ( SetException), а также обрабатывать подключения продолжений. редактировать await-ы, которые еще не завершены ( AwaitOnCompleted/ AwaitUnsafeOnCompleted). В случае с System.Threading.Tasks.Task. он по умолчанию связан с AsyncTaskMethodBuilder. Обычно эта ассоциация обеспечивается через [AsyncMethodBuilder(...)] атрибут, примененный к типу, но Task-е она известна специально для C# и поэтому фактически не дополняется этим атрибутом. Таким образом, компилятор обратился к компоновщику для использования этого async метода и создает его экземпляр, используя Create метод, который является частью шаблона. Обратите внимание, что, как и в случае с конечным автоматом, AsyncTaskMethodBuilder он также является структурой, поэтому здесь также нет выделения.

Затем конечный автомат заполняется аргументами этого метода точки входа. Эти параметры должны быть доступны для тела метода, который был перемещен в MoveNext, и поэтому эти аргументы должны храниться в конечном автомате, чтобы на них можно было ссылаться в коде при последующем вызове MoveNext. Конечный автомат также инициализируется в исходном -1 состоянии. Если MoveNext вызывается и состояние равно -1, мы логически начнем с начала метода.

Теперь самая скромная, но самая важная линия: вызов метода строителя Start. Это еще одна часть шаблона, которая должна быть представлена ​​в типе, используемом в возвращаемой позиции метода async, и используется для выполнения начальных значений MoveNext на конечном автомате. Метод Start компоновщика фактически выглядит следующим образом:

-6

так что вызов stateMachine.<>t__builder.Start(ref stateMachine); на самом деле просто вызов stateMachine.MoveNext(). В таком случае, почему компилятор просто не выдает это напрямую? Зачем Start вообще? Ответ в том, что здесь есть нечто большее чем Start, чем я предполагаю. Но для этого нам нужно сделать небольшой экскурс в понимание ExecutionContext.

Источник

to be continued...