Конечный автомат и его внутреннее устройство
Примечание переводчика:
State Machine, конечный автомат это преобразованный async метод. Компилятор преобразует метод в тип, реализующий конечный автомат (наследуется от IAsyncStateMachine). Благодаря такому механизму, при достижении первого оператора await поток, начавший метод, может возвращаться без «физического» оператора return метода, тем самым, продолжая выполнение основной программы.
В математике, конечный автомат это некоторая система, которая может находится только в одном состоянии.
(Возможные) состояния конечного автомата:
-1— Начальное состояние (Initial State): Это состояние до начала выполнения метода. Когда выполнение только начинается, автомат находится именно в этой точке.
0, 1, 2...— Промежуточные состояния (Intermediate States): Каждому ключевому словуawaitв вашем методе присваивается уникальное числовое состояние (начиная с 0). Когда выполнение доходит до ожидания и метод приостанавливается, автомат запоминает это число. Как только ожидаемая операция завершается, он «просыпается» и, глядя на это число, точно знает, в каком месте кода нужно продолжить выполнение и какие локальные переменные восстановить .
-2— Конечное состояние (Final State): Это состояние сигнализирует о том, что метод полностью завершил свою работу. Неважно, успешно ли он выполнился или выбросил исключение — после того, как работа закончена, состояние устанавливается в-2.Перед выполнением асинхронной операции компилятор, встречающий оператор await, берет указанный операнд и пытается вызвать для него метод GetAwaiter. Awaiter это объект ожидания, который мы получаем от GetAwaiter. Объект ожидания будет ждать завершения асинхронной операции и возвращать ее результат.
Аналогия: пицца — асинхронная операция. Оператор await — ожидать пиццу. Объект ожидания awaiter — доставщик пиццы.
Построитель (например,
AsyncTaskMethodBuilder<T>) — это внутренний механизм компилятора, который конструирует и управляет жизненным циклом объектаTaskдля асинхронного метода
Простыми словами, async/await это своего рода синтаксический сахар. Каждый асинхронный метод будет преобразован в StateMachine, и затем вызывающий метод использует ее для выполнения бизнес‑логики.
Некоторым нравится сначала теория, некоторые хотят сразу увидеть код. Я планирую использовать гибридный подход: сначала небольшая доза теории, затем весь код для конечного автомата (с полезными комментариями), а потом попробуем нарисовать схему, чтобы объяснить алгоритм выполнения кода внутри конечного автомата.
Несколько терминов, которые будут использоваться в статье:
WorkerFunction: метод, который будет выполнять фактическую асинхронную работу.
CallingFunction: метод, который будет вызывать
WorkerFunction.FirstCall: первый вызов метода
MoveNextу конечного автомата (синхронный поток выполнения).WakeUpCall: момент, когда результаты операции
awaitстановятся доступными, и код продолжает выполнение с того места, где он остановился. Своего рода callback (обратный вызов).
Что происходит при компиляции кода (кратко — теория)
Мы берем наш фрагмент кода и вставляем его на sharplab, а затем он генерирует скомпилированный код для этого фрагмента. Вот несколько вещей, которые компилятор сгенерирует для нашего асинхронного кода:
Компилятор сгенерирует код конечного автомата (реализующий
IAsyncStateMachine) дляWorkerFunction.Перенесет фактическую логику
WorkerFunctionв функциюMoveNext.Создаст внутри конечного автомата переменные, необходимые для его работы.
Изменит
CallingFunctionтак, чтобы он создавал новый экземплярStateMachine.Вызовет
Startу одного из генераторов задач (TaskMethodGenerator) конечного автомата внутриCallingFunction(подробнее ниже).
Теперь посмотрим на код
Возьмем очень простой фрагмент кода, в котором используются ключевые слова async/await. Я намеренно сохраняю сложность кода минимальной, так как нам нужно понять работу async/await, а не возможные варианты применения асинхронности. Это заслуживает отдельной статьи.
using System; using System.Diagnostics; using System.Net.Http; using System.Threading.Tasks; namespace Scenario { class Program { static void Main(string[] args) { try { AsyncDownload().GetAwaiter().GetResult(); Console.ReadLine(); } catch (Exception e) { Console.WriteLine(e); throw; } } static async Task<string> AsyncDownload() { HttpClient client = new HttpClient(); //Асинхронно скачиваем контент веб-страницы return await client.GetStringAsync("https://msdn.microsoft.com"); } } }
Я вставил этот код на sharplab и скомпилировал его в режиме Debug. Полученный сгенерированный результат представлен ниже:
using System; using System.Diagnostics; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Security; using System.Security.Permissions; using System.Threading.Tasks; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] namespace Scenario { internal class Program { // Создан конечный автомат для представления асинхронного метода загрузки // с использованием HTTP-клиента [CompilerGenerated] private sealed class <AsyncDownload>d__1 : IAsyncStateMachine { // Переменная для поддержания текущего состояния выполнения конечного автомата // FirstCall: Начальное значение -1 public int <>1__state; // Построитель для создания новой асинхронной задачи, которая будет выполнять этот код конечного автомата public AsyncTaskMethodBuilder<string> <>t__builder; // HTTP-клиент, используемый методом для загрузки содержимого удаленного URL-адреса private HttpClient <client>5__1; // Переменная для хранения результатов вызова HttpClient private string <>s__2; // Awaiter задачи по загрузке содержимого с помощью HTTP-клиента private TaskAwaiter<string> <>u__1; // Это раздел, где находится фактическая логика исходного метода // Содержит логику выполнения кода до оператора await // Также настраивает параметры вызова функции пробуждения // После завершения выполнения асинхронного метода private void MoveNext() { // Копирует текущее состояние в локальную переменную // Начальное значение состояния будет -1 int num = <>1__state; // Переменная для сохранения результата HTTP-запроса string result; try { // Переменная для сохранения объекта ожидания для новой задачи TaskAwaiter<string> awaiter; // В первый раз num будет -1 if (num != 0) { //FirstCall: мы окажемся здесь по первичному вызову <client>5__1 = new HttpClient(); //FirstCall: Сохранит объект ожидания для HTTP-вызова в переменную awaiter = <client>5__1.GetStringAsync("https://msdn.microsoft.com").GetAwaiter(); //FirstCall: Скорее всего, мы перейдем к этому блоку при первом вызове //FirstCall: Этот блок предназначен для оптимизации в случае, если задача уже выполнена //FirstCall: Мы пропускаем планирование вызова пробуждения или продолжения if (!awaiter.IsCompleted) { //FirstCall: Мы устанавливаем переменную состояния в 0 //FirstCall: Чтобы при вызове функции пробуждения или обратном вызове мы вообще не входили в этот блок. num = (<>1__state = 0); <>u__1 = awaiter; <AsyncDownload>d__1 stateMachine = this; //FirstCall: Именно здесь происходит большая часть магии при вызове AwaitUnsafeOnCompleted //FirstCall: На этом шаге мы регистрируем конечный автомат как продолжение задачи, вызывая AwaitUnsafeOnCompleted //Но как это делается? //builder.AwaitUnsafeOnCompleted выполняет несколько действий в фоновом режиме //FirstCall: 1. TaskMethodBuilder захватывает контекст выполнения //FirstCall: 2. Создает MoveNextAction, используя контекст выполнения //FirstCall: 3. Этот MoveNextAction вызовет MoveNext конечного автомата и предоставит контекст выполнения //FirstCall: 4. Устанавливает MoveNextAction в качестве обратного вызова для объекта ожидания при завершении, используя awaiter.UnsafeOnCompleted(action) <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine) //FirstCall: Освободить процессор или поток для вызывающей стороны //FirstCall: Начинается работа "ожидания" return; } } else { //WakeupCall: Мы получаем объект ожидания из контекста выполнения awaiter = <>u__1; //WakeupCall: Установить значение объекта ожидания в null для освобождения памяти <>u__1 = default(TaskAwaiter<string>); //WakeupCall: Временно установить переменную состояния в значение -1 (начальное состояние) num = (<>1__state = -1); } //WakeupCall: Получить результат от объекта ожидания //WakeupCall: Объект ожидания должен завершиться, как только мы получим WakeUpCall <>s__2 = awaiter.GetResult(); result = <>s__2; } catch (Exception exception) { <>1__state = -2; <>t__builder.SetException(exception); return; } //WakeUpCall: Установить состояние на конечное, на этом всё <>1__state = -2; //WakeUpCall: Построитель завершает задачу и устанавливает её результат <>t__builder.SetResult(result); } void IAsyncStateMachine.MoveNext() { //ILSpy сгенерировал эту явную реализацию интерфейса из директивы .override в MoveNext this.MoveNext(); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { //ILSpy сгенерировал эту явную реализацию интерфейса из директивы .override в SetStateMachine this.SetStateMachine(stateMachine); } } // Далее, мы будем называть функцию Main как "CallingFunction" private static void Main(string[] args) { try { AsyncDownload().GetAwaiter().GetResult(); Console.ReadLine(); } catch (Exception value) { Console.WriteLine(value); throw; } } // Мы будем называть этот метод "WorkerMethod" [AsyncStateMachine(typeof(<AsyncDownload>d__1))] [DebuggerStepThrough] private static Task<string> AsyncDownload() { //FirstCall: Создать новый экземпляр конечного автомата <AsyncDownload>d__1 stateMachine = new <AsyncDownload>d__1(); //FirstCall: Создать новый экземпляр AsyncTaskMethodBuilder и настроить его для конечного автомата stateMachine.<>t__builder = AsyncTaskMethodBuilder<string>.Create(); //FirstCall: Установить состояние конечного автомата в -1 (начальное) stateMachine.<>1__state = -1; AsyncTaskMethodBuilder<string> <>t__builder = stateMachine.<>t__builder; //FirstCall: Вызовет метод Start в построителе, который приведет к вызову StateMachine.MoveNext(); <>t__builder.Start(ref stateMachine); //FirstCall: Возвращает задачу из построителя от WorkerMethod return stateMachine.<>t__builder.Task; } } }
Пояснение к приведенному выше коду
Если вы уже ознакомились с приведенным выше примером кода, и вам все еще что-то непонятно, предлагаю изучить мою схему. Это может помочь лучше понять ход выполнения кода.
Я использую цветовые паттерны для лучшего понимания.
Все блоки с красной рамкой будут выполнены как при первом вызове FirstCall, так и при вызове пробуждения WakeUpCall.
Синие блоки будут выполнены только при FirstCall.
Зеленые блоки могут быть выполнены при FirstCall, если объект ожидания уже завершил операцию, но это крайне маловероятно, поскольку в этом процессе нет оптимизаций.
Зеленые блоки будут выполнены при вызове WakeUpCall в случае отсутствия ошибок или исключений.

Также по этой теме рекомендую к прочтению статью «Другой способ понять, как работает async/await в C#»
