Async-Await в C# это синтаксический сахар для конечного автомата

в 7:23, , рубрики: async, await, awaiter, C#, c#.net, statemachine, taskbuilder, асинхронность, конечный автомат

Конечный автомат и его внутреннее устройство

Примечание переводчика:

  • 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, а затем он генерирует скомпилированный код для этого фрагмента. Вот несколько вещей, которые компилятор сгенерирует для нашего асинхронного кода:

  1. Компилятор сгенерирует код конечного автомата (реализующийIAsyncStateMachine) для WorkerFunction.

  2. Перенесет фактическую логику WorkerFunction в функцию MoveNext.

  3. Создаст внутри конечного автомата переменные, необходимые для его работы.

  4. Изменит CallingFunction так, чтобы он создавал новый экземпляр StateMachine.

  5. Вызовет 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#»

Автор: daryaKarman

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js