Конечный автомат и его внутреннее устройство
Примечание переводчика:
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#»
ссылка на оригинал статьи https://habr.com/ru/articles/1035442/