Async/await. Куда делся thread.  Инверсия асинхронности. Разоблачение мистификации

В прошлый раз мы разобрали пример, когда асинхронная операция использует дополнительный поток. Этот пример многим показался провокационным и даже вредным, что для меня выглядит достаточно странным. Насколько я понял основной претензией является то, что этот пример для многих как бы отрицает «экономное использование потоков», как это сформулировано например здесь-«metanit: Асинхронное программирование» .

Конечно, многие обиделись на меня за то, что я посмел возражать признанному авторитету, который вынес в заголовок своей очень известной работы фразу There is no thread (Там нет потока) ведь хорошо известно, что: «нет пророка в своем отечестве», и, видимо, быть не должно, но это все эмоции.

Давайте я покажу, как преобразовать мой пример, чтобы он продемонстрировал не только то самое «экономное использование потоков», но и откуда оно берется.

Прежде чем разбирать примеры давайте попробуем сформулировать (или вспомнить?) что же такое асинхронность, это нам пригодится для анализа наших примеров.

 Можно, например вспомнить определения синхронного и асинхронного методов, которое дал Стивен Тоуб (например):

this method is «synchronous» because a caller will not be able to do anything else until this whole operation completes and control is returned back to the caller

this method is «asynchronous» because control is expected to be returned back to its caller very quickly and possibly before the work associated with the whole operation has completed

По сути, тут прилагательное асинхронный (или синхронный) определяет, как и когда заканчивается работа (или операция) внутри метода: одновременно с возвращением из метода или не одновременно. Асинхронность, по сути, разделяет-расщепляет понятие метода на метод как функцию кода, и работу или операцию, которую эта функция запускает на исполнение и которая может продолжаться в других методах или даже на аппаратном уровне. Если про синхронные методы-функции можно говорить, что они выполняют свою работу-операцию, в смысле выполняют от начала и до конца, и поэтому они по сути являются синонимом той операции, которую они выполняют, то асинхронные методы-функции можно ассоциировать с выполняемой ими операцией лишь частично, так как они только запускают эту операцию, и операция выполняется как бы сама по себе уже после того, как асинхронная функция отработала.

 Рассмотрим, например, синхронную операцию, например вывод в консоль и соответствующий ей синхронный метод:

Console.WriteLine(string);

Почему эта операция синхронная или по-другому блокирующая? Потому что мы можем быть уверены, что строка появится на консоли не позднее момента, когда эта функция вернет управление в вызвавшем ее коде. Другими словами, исполнение программы заблокировано до получения результата операции, которую выполняет эта функция.

 Здесь можно почитать про неблокирующую, то есть асинхронную запись в консоль (If it does block is there a method of writing asynchronous output to the Console?):

https://stackoverflow.com/questions/3670057/does-console-writeline-block

Сложность понятия асинхронности, рассуждения для гурманов с уклоном в философию

Пару недель назад на Хабре прошла статья

 https://habr.com/ru/articles/799145/  

с попыткой разобрать семантику асинхронности через аналогию с симфоническим оркестром. Но зачем нужны аналогии, которые всегда хромают, когда можно просто взять и напрямую растолковать понятие асинхронности/синхронности применительно к методу (функции) с кодом произвольного языка.

Начать надо с того, что сложность асинхронности как термина очевидно связана с тем, что,

Во-первых, это противоположность синхронности, а, во-вторых, синхронность не существует сама по себе, синхронность всегда существует между двумя (и более) сущностями, между двумя объектами, обычно объектами действия, которые в свою очередь существуют во времени. Синхронность можно определить как: соответствие поведения объектов действия по параметрам времени.

Тут мы вводим еще одно понятие «объект действия». Применительно к программированию это вполне интуитивное обобщение для всего, что каким-то образом определяет действие. Это в первую очередь методы или функции, которые инкапсулируют вычисления или логику преобразования данных/обмена данными/логику управления чем-то, в том числе преобразованием-обменом данными во времени, то есть действия по отношению к данным или в общем по управлению чем-то. Иногда в качестве объектов действия в программировании рассматриваются задачи, процедуры, абстрактная работа, потоки исполнения, запросы…

Объекты действия и просто объекты из ООП

Я всегда хотел поделиться с аудиторией своим пониманием объекта для Объектно-Ориентированного Программирования (ООП). Мне нравится представлять себе объект как сокращение выражения «объект нашего внимания». По-моему, очень последовательно-логично получается, когда мы выделяем при проектировании то, что требует нашего внимания как объект, который мы должны каким-то образом идентифицировать-изолировать в коде. В этом смысле любой тип программирования можно рассматривать как спецификацию ООП, например процедурное или функциональное программирование это способ программирования, когда основным и базовым объектом проектирования-реализации алгоритмов/логики являются процедуры/функции. Когда мы сосредоточились на объектах действия таких как методы, они же функции или процедуры, мы в какой-то степени попали в зону функционального-процедурного программирования. Здесь важно отметить, что вряд ли хороший дизайн может обойтись объектами только одного типа, например только процедурами или только объектами действия, такое сокращение типов объектов при построении системы вряд ли позволит создать жизнеспособную систему. Предлагаю считать это просто упражнением по риторике, которое позволяет укрепить и стабилизировать понимание отношений между терминами и определениями, которые нам придется использовать далее.

 Подведем промежуточный итог. В чем сложность асинхронности? Ответ: мы должны рассматривать взаимодействие двух и более объектов во времени, фактически это всегда анализ некоторой системы не статических во времени объектов. Мы должны выяснить тип объектов, мы должны выяснить параметры, которые влияют/участвуют во взаимодействии, определить способ их согласования-синхронизации.

Модифицированный пример, есть ли теперь дополнительный поток

Я думаю, теперь вы готовы анализировать примеры. Наша цель понять как же async/await «экономят потоки», в этот раз мы доберемся до примера, в котором что-то подобное происходит.

Пример из прошлой статьи очевидно запускает дополнительный поток для вызванной из функции main () консольного приложения асинхронной операции:

SomeMethodAsync1();

Против фактов не попрешь, как говорится, но что, если мы поставим await перед этим вызовом?

static async Task Main()
        {
//ДОБАВЛЕН AWAIT!!!!!!!!!!!
            await SomeMethodAsync1();//ТУТ ДОБАВЛЕН AWAIT!!!!!!!!!!!
            Console.WriteLine($"ThrID={Thread.CurrentThread.ManagedThreadId}");
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine($"MAIN Iter={i}");
                Thread.Sleep(100);
            }
        }

        static async Task SomeMethodAsync1()
        {
            await Task.Yield();
            Console.WriteLine($"TrID={Thread.CurrentThread.ManagedThreadId}");
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine($"MethodIter={i}");
                Thread.Sleep(100);
            }
        }

Нет ничего проще чем это проверить, спасибо всем, кто комментировал мою предыдущую статью, потому что именно ваши комментарии заставили меня пойти на этот ужасный эксперимент :)))!!!

Кстати, обратите внимание, у нашей функции майн уже стоит необходимый async, который нужен чтобы скомпилировать функцию с добавленным await-ом.

Мы получим в этом случае вот такой замечательный вывод:

вывод для вызова под AWAIT-ом

вывод для вызова под AWAIT-ом

Как видите теперь все исполнение происходит в единственном потоке с TrID=5. Сравните с тем, что было без единственного добавленного слова await:

вывод для вызова без AWAIT-а (из предыдущей статьи)

вывод для вызова без AWAIT-а (из предыдущей статьи)

Преобразование асинхронного вызова в синхронный

Вы можете видеть, что добавление ключевого слова await перед вызовом Асинхронной функции превратило асинхронный вызов в синхронный. В соответствии с определением синхронности/асинхронности, которое мы разобрали в начале, вывод, который производит функция SomeMethodAsync1() при а-синхронном вызове происходит уже после самого вызова и поэтому он смешивается с выводом, который формирует вызывающая функция после вызова метода SomeMethodAsync1(). Я думаю, не надо пояснять что порядок следования записей сформированных в двух этих функциях при асинхронном вызове никогда точно не определен, в этом смысле асинхронность порождает хаос (хотя при ближайшем рассмотрении мы бы увидели, что это не совсем хаос, это все-таки порядок, с элементами хаоса). Просто первое, что пришло мне в голову при сравнении этих двух выводов, прокомментировать это сравнение броским: «Порядок против Хаоса», это конечно преувеличение, лишь с маленькой долей истины.

Когда мы добавили AWAIT перед вызываемой SomeMethodAsync1() функцией мы сделали вызов этой асинхронной функции снова синхронной, мы вернули порядок и в коде, и в выводе (хотя наверно только в выводе, в коде, по сути, ничего не изменилось), который он генерирует. Await блокирует выполнение вызывающей функции до окончания асинхронной функции, соответственно функция у нас осталась асинхронной, но вызов мы организовали для нее синхронный. Мне кажется вполне можно назвать такое преобразование асинхронного метода в синхронный инверсией асинхронности, романтично звучит :), а главное коротко.

 Кажется, что получилась какая-то бессмыслица, зачем писать асинхронную функцию, а потом прилагать какие-то дополнительные усилия чтобы вызвать ее в синхронном режиме?

На самом деле это очень востребованное преобразование на практике. Практически все примеры для async/await по сути демонстрируют необходимость такого преобразования. Только почему то никто не решается называть Синхронный вызов а-синхронной функции Синхронным, наверно именно потому, что на первый взгляд это звучиит как бессмыслица.

Но если разобрать что же тут произошло внимательно, как подобает профессиональному разработчику и аналитику мы увидим ожидаемую экономию потока.

Для начала посмотрим схему потоков, которая получается при вызове нашей асинхронной функции SomeMethodAsync1 без AWAIT-а:

схема генерации записей при честном асинхронном вызове

схема генерации записей при честном асинхронном вызове

Как же изменилась эта схема при добавлении ключевого слова AWAIT перед вызовом? Но это не единственный вопрос, на который надо ответить, чтобы понять, а в чем же заключается «экономия потоков». Поэтому я нарисовал сразу две схемы того, как можно превратить асинхронный вызов обратно в синхронный. Первый вариант, который я назвал гипотетическим, на самом деле вполне практический, поскольку я уверен он достаточно широко использовался и наверняка где-то продолжает работать, а второй вариант — тот который нам предлагает применение async/await:

два варианта преобразования асинхронного вызова в синхронный

два варианта преобразования асинхронного вызова в синхронный

Я думаю вы догадались что PAsync на второй картинке означает Преобразованный Асинхронный вызов, потому что его явно нельзя в этом случае назвать, например, псевдо-асинхронным вызовом, он очевидно является синхронным.

Надеюсь из сравнения этих двух картинок становится совершенно понятно в чем заключается пресловутое «экономное использование потоков», и просто более эффективное построение работы с методами которые изначально реализованы как а-синхронные. AWAIT позволяет нам использовать активный поток в котором происходит вызов асинхронной функции чтобы выполнить эту функцию когда мы хотим просто дождаться чтобы эта асинхронная функция выполнила свою работу до конца. Как видите добавленный AWAIT (который нельзя добавить без ASYNC) позволил нам исключить-сэкономить дополнительный поток для асинхронной операции, когда мы решили вызвать ее синхронно.

Далее можно разобрать пример с совершенно идентичной логикой в контексте визуального приложения, там тоже много чего интересного можно подчерпнуть. Я в принципе уже начал разбирать, но уже слишком много материала и идей для одной статьи было бы. Очень интересно какая будет оценка у этой, к чему будут придираться критики на сей раз.

С уважением,

Сёргий

© Habrahabr.ru