что такое асинхронный код блока

Как я понимаю асинхронный код?

Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Асинхронный код становится все более популярным для написания отзывчивых приложений. К сожалению, асинхронное программирование так же привносит дополнительные трудности. Как следствие, понять, как работает такой код, может быть непростой задачей, вне зависимости от вашего опыта. Если вы только начали работать с асинхронным кодом, или вы захотели освежить свое понимание – это введение в мир асинхронного программирования!

Что такое асинхронный код?

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

Вам может показаться, что асинхронный код очень похож на многопоточный, ведь, в конце концов, множество методов может выполняться одновременно в обоих случаях. В действительности, асинхронное программирование может использоваться вместе как с однопоточными, так и с многопоточными приложениями. Это значит, что у вас может быть однопоточная асинхронная программа, в которой один поток может запускать параллельные задачи. И наоборот, у вас также может быть многопоточное асинхронное приложение, в котором несколько потоков могут запускать несколько параллельных задач.

Почему мне стоит использовать асинхронный код? Пример, пожалуйста!

Чтобы иметь какую-то аналогию для демонстрации асинхронного программирования, рассмотрим процесс выпечки пирога. Этот процесс будет представлен потоком, который выполняет несколько шагов (или задач), как показано в коде ниже. Этот код корректен, и у вас все равно получится вкусный пирог после выполнения метода. Однако, поскольку весь код является синхронным, каждая строка будет выполняться последовательно. Другими словами, вы будете стоять совершенно неподвижно, ожидая, пока печь завершит предварительный нагрев. А ведь в это же самое время вы могли бы сделать тесто для вашего пирога!

Синхронный метод MakeCake()

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Синхронная программа выпекания пирога

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

В реальной жизни вы, как правило, разделяете этот процесс на задачи, замешиваете тесто, пока духовка разогревается. Или делаете глазурь, в то время как пирог запекается в духовке. Это увеличивает вашу производительность и позволяет испечь торт намного быстрее. Это как раз тот случай, где асинхронный код пригодится! Сделав наш текущий код асинхронным, мы сможем заняться другими делами, чтобы скоротать время, в то время пока мы ожидаем(await) результата задачи(task), такой как выпекание пирога в духовке.
Чтобы сделать это – изменим наш код, а так же добавим метод PassTheTime. Теперь наш код сохраняет состояние задачи, запускает другую синхронную или асинхронную операцию и получает результат сохраненной задачи, в тот момент, когда это необходимо.

Асинхронный метод MakeCake()
что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Асинхронная программа выпечки пирога
что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

По сравнению с синхронным методом MakeCake, в котором отсутствует метод PassTheTime, асинхронному методу MakeCakeAsync удается выполнить больше задач, не блокируя поток, что сокращает время, необходимое для выполнения всего метода в целом.

Сравнение асинхронной и синхронной программ

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

C # позволяет писать асинхронный код, используя тип Task и ключевые слова await и async. Тип Task сообщает вызывающей стороне о возможном типе возвращаемого значения. Он также указывает на то, что другие действия могут продолжать выполняться в вызвавшем его методе. Ключевое слово async работает в паре с ключевым словом await, которое уведомляет компилятор о том, что нам потребуется возвращаемое методом значение, но не сразу. В результате нам не нужно блокировать вызывающий поток, и мы можем продолжать выполнение других задач, пока не потребуется ожидаемое значение. Первоначально асинхронный метод будет выполняться синхронно, пока не будет найдено ключевое слово await. Это именно тот момент, когда выполнение метода начнется асинхронно.

Я узнал об асинхронном коде! Что теперь?

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

Приложения, использующие HTTP-запросы — в зависимости от запроса, обработка HTTP-вызовов может занять длительное время. Использование асинхронного кода позволяет нам выполнить другие операции, пока мы ожидаем ответа от сервера.

Пример запроса HTTP GET

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

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

Источник

Введение в асинхронный JavaScript

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

Необходимое условие:Базовая компьютерная грамотность, достаточное понимание основ JavaScript.
Цель:Ознакомиться с тем, что такое асинхронный JavaScript, чем он отличается от синхронного и в каких случаях используется.

Синхронный JavaScript

Чтобы (позволить нам) понять что есть асинхронный JavaScript, нам следовало бы для начала убедиться, что мы понимаем что такое синхронный JavaScript. Этот раздел резюмирует некоторую информацию из прошлой статьи.

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

В этом блоке кода команды выполняются одна за другой:

Пока выполняется каждая операция, ничего больше не может произойти — обработка (отображение) документа приостановлена. Так происходит, как было сказано в предыдущей статье, потому что JavaScript является однопоточным. В каждый момент времени может выполняться только одна команда, обрабатываемая в единственном — главном потоке. Все остальные действия блокируются до окончания выполнения текущей команды.

Так и в примере выше: после нажатия кнопки абзац не сможет появиться пока не будет нажата кнопка OK в окне сообщения. Попробуйте сами:

Асинхронный JavaScript

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

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

Это происходит потому что вы не знаете сколько времени займёт загрузка картинки, следовательно, когда вы начнёте выполнять вторую строку кода, сгенерируется ошибка (возможно, периодически, возможно, каждый раз), потому что response ещё не доступен. Вместо этого, ваш код должен дождаться возвращения response до того, как попытается выполнить дальнейшие инструкции.

Есть два типа стиля асинхронного кода, с которыми вы столкнётесь в коде JavaScript, старый метод — колбэки (callbacks) и более новый — промисы (promises). В следующих разделах мы познакомимся с каждым из них.

Асинхронные колбэки

Асинхронные колбэки — это функции, которые определяются как аргументы при вызове функции, которая начнёт выполнение кода на заднем фоне. Когда код на заднем фоне завершает свою работу, он вызывает колбэк-функцию, оповещающую, что работа сделана, либо оповещающую о трудностях в завершении работы. Обратные вызовы — немного устаревшая практика, но они все ещё употребляются в некоторых старомодных, но часто используемых API.

Пример асинхронного колбэка вторым параметром addEventListener() (как мы видели выше):

Первый параметр — тип обрабатываемого события, второй параметр — колбэк-функция, вызываемая при срабатывании события.

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

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

Заметьте, что не все колбэк-функции асинхронны — некоторые запускаются синхронно. Например, при использовании Array.prototype.forEach() для перебора элементов массива (запустите пример, и посмотрите исходный код):

В этом примере мы перебираем массив с именами греческих богов и выводим индексы и значения в консоль. Ожидаемый параметр для forEach() — это Колбэк-функция, которая содержит два параметра: ссылку на имя массива и значения индексов. Однако эта функция не ожидает никаких действий — она запускается немедленно.

Промисы

Примечание: вы можете посмотреть законченную версию на github (посмотрите исходный код и запустите пример).

В примере видно, как fetch() принимает один параметр — URL ресурса, который нужно получить из сети, — и возвращает промис. Промис — это объект, представляющий асинхронную операцию, выполненную удачно или неудачно. Он представляет собой как бы промежуточное состояние. По сути, это способ браузера сказать: «я обещаю вернуться к вам с ответом как можно скорее», поэтому в дословном переводе «промис» (promise) означает «обещание».

Может понадобиться много времени, чтобы привыкнуть к данной концепции; это немного напоминает Кот Шрёдингера в действии. Ни один из возможных результатов ещё не произошёл, поэтому операция fetch в настоящее время ожидает результата. Далее у нас есть три блока кода следующих сразу после fetch() :

Примечание: вы узнаете намного больше о promise позже в этом модуле, так что не волнуйтесь если вы что-нибудь не поняли.

Очередь событий

Промисы и колбэк-функции

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

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

Природа асинхронного кода

Давайте рассмотрим пример, который дополнительно иллюстрирует природу асинхронного кода, показывая, что может произойти, когда мы не полностью осознаем порядок выполнения кода, и проблемы, связанные с попыткой трактовать асинхронный код как синхронный. Следующий пример довольно похож на тот, что мы видели раньше (запустите пример, и посмотреть исходный код). Одно из отличий состоит в том, что мы включили ряд операторов console.log() чтобы проиллюстрировать порядок, в котором, как вы думаете, будет выполняться код.

Если вы запутались, рассмотрим следующий небольшой пример:

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

В менее простом примере кода такая система может вызвать проблему — вы не можете включить блок асинхронного кода, который возвращает результат, на который вы потом будете полагаться в блоке синхронного кода. Вы просто не можете гарантировать, что асинхронная функция вернётся до того, как браузер обработает синхронный блок.

Чтобы увидеть это в действии, попробуйте взять локальную копию нашего примера и измените третий вызов console.log () следующим образом:

Теперь вместо третьего сообщения должна возникнуть следующая ошибка:

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

Активное обучение: сделайте все это асинхронно!

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

Заключение

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

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

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

Если мы выполняем операцию, которая требует времени, например, запрос к базе данных и использование полученных результатов для заполнения шаблонов, лучше вытолкнуть это из основного потока и выполнить задачу асинхронно. Со временем вы узнаете, когда имеет смысл выбирать асинхронную технику вместо синхронной.

Источник

JavaScript: методы асинхронного программирования

Синхронный код на JavaScript, автор которого не стремился сбить с толку тех, кто этот код будет читать, обычно выглядит просто и понятно. Команды, из которых он состоит, выполняются в том порядке, в котором они следуют в тексте программы. Немного путаницы может внести поднятие объявлений переменных и функций, но чтобы превратить эту особенность JS в проблему, надо очень постараться. У синхронного кода на JavaScript есть лишь один серьёзный недостаток: на нём одном далеко не уехать.

что такое асинхронный код блока. 0e11e33e470f496baed4267be68fcf86. что такое асинхронный код блока фото. что такое асинхронный код блока-0e11e33e470f496baed4267be68fcf86. картинка что такое асинхронный код блока. картинка 0e11e33e470f496baed4267be68fcf86. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Практически каждая полезная JS-программа написана с привлечением асинхронных методов разработки. Здесь в дело вступают функции обратного вызова, в просторечии — «коллбэки». Здесь в ходу «обещания», или Promise-объекты, называемые обычно промисами. Тут можно столкнуться с генераторами и с конструкциями async/await. Асинхронный код, в сравнении с синхронным, обычно сложнее писать, читать и поддерживать. Иногда он превращается в совершенно жуткие структуры вроде ада коллбэков. Однако, без него не обойтись.

Сегодня предлагаем поговорить об особенностях коллбэков, промисов, генераторов и конструкций async/await, и подумать о том, как писать простой, понятный и эффективный асинхронный код.

О синхронном и асинхронном коде

Начнём с рассмотрения фрагментов синхронного и асинхронного JS-кода. Вот, например, обычный синхронный код:

Он, без особых сложностей, выводит в консоль числа от 1 до 3.

Теперь — код асинхронный:

Возможно, если вы только начинаете путь JS-разработчика, вы зададитесь вопросами: «Зачем это всё? Может быть, можно переделать асинхронный код в синхронный?». Поищем ответы на эти вопросы.

Постановка задачи

Предположим, перед нами стоит задача поиска пользователя GitHub и загрузки данных о его репозиториях. Главная проблема тут в том, что мы не знаем точного имени пользователя, поэтому нам нужно вывести всех пользователей с именами, похожими на то, что мы ищем, и их репозитории.

В плане интерфейса ограничимся чем-нибудь простым.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.
Простой интерфейс поиска пользователей GitHub и соответствующих им репозиториев

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

Функции обратного вызова

С функциями в JS можно делать очень много всего, в том числе — передавать в качестве аргументов другим функциям. Обычно так делают для того, чтобы вызвать переданную функцию после завершения какого-то процесса, который может занять некоторое время. Речь идёт о функциях обратного вызова. Вот простой пример:

Используя этот подход для решения нашей задачи, мы можем написать такую функцию request :

Разберём то, что здесь происходит:

Если придать нашему коду более завершённый вид, снабдить его средствами обработки ошибок и отделить определение функций обратного вызова от кода выполнения запроса, что улучшит читабельность программы, получится следующее:

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.
Ад коллбэков во всей красе. Изображение взято отсюда.

В данном случае под «состоянием гонки» мы понимаем ситуацию, когда мы не контролируем порядок получения данных о репозиториях пользователей. Мы запрашиваем данные по всем пользователям, и вполне может оказаться так, что ответы на эти запросы окажутся перемешанными. Скажем, ответ по десятому пользователю придёт первым, а по второму — последним. Ниже мы поговорим о возможном решении этой проблемы.

Промисы

Используя промисы можно улучшить читабельность кода. В результате, например, если в ваш проект придёт новый разработчик, он быстро поймёт, как там всё устроено.

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

Разберём этот пример:

Для того, чтобы не погрязнуть в теории, вернёмся к нашему примеру. Перепишем его с использованием промисов.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.
Это — промис в состоянии ожидания. Он может быть либо успешно разрешён, либо отклонён

Благодаря такому подходу мы разобрались с состоянием гонки и с некоторыми возникающими при этом проблемами. Ада коллбэков тут не наблюдается, но код пока ещё читать не так-то легко. На самом деле, наш пример поддаётся дальнейшему улучшению за счёт выделения из него объявлений функций обратного вызова:

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

Генераторы

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

Посмотрим теперь, как это всё применить к нашей задаче. Итак, вот функция request :

Генератор будет выглядеть так:

Вот что здесь происходит:

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

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

Надо отметить, что генераторы — возможность сравнительно новая, как результат, если вы рассчитываете на использование вашего кода в старых версиях браузеров, код надо обработать транспилятором. Кроме того, генераторы в написании асинхронного кода используют нечасто, поэтому, если вы занимаетесь командной разработкой, учтите, что некоторые программисты могут быть с ними незнакомы.
На тот случай, если вы решили лучше вникнуть в эту тему, вот и вот — отличные материалы о внутреннем устройстве генераторов.

Async/await

Здесь происходит следующее:

Этот подход и использование промисов — мои любимые методы асинхронного программирования. Код, написанный с их использованием, удобно и читать и править. Подробности об async/await можно почитать здесь.

Итоги

В зависимости от особенностей поставленной перед вами задачи, может оказаться так, что вы будете пользоваться async/await, коллбэками, или некоей смесью из разных технологий. На самом деле, ответ на вопрос о том, какую именно методику асинхронной разработки выбрать, зависит от особенностей проекта. Если некий подход позволяет решить задачу с помощью читабельного кода, который легко поддерживать, который понятен (и будет понятен через некоторое время) вам и другим членам команды, значит этот подход — то, что вам нужно.

Уважаемые читатели! Какими методиками написания асинхронного кода на JavaScript вы пользуетесь?

Источник

Асинхронность в программировании

В области разработки высоконагруженных многопоточных или распределенных приложений часто возникают дискуссии об асинхронном программировании. Сегодня мы подробно погрузимся в асинхронность и изучим, что это такое, когда она возникает, как влияет на код и язык программирования, которым мы пользуемся. Разберемся, зачем нужны Futures и Promises и затронем корутины и операционные системы. Это сделает компромиссы, возникающие во время разработки ПО, более явными.

В основе материала — расшифровка доклада Ивана Пузыревского, преподавателя школы анализа данных Яндекса.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Видеозапись

1. Содержание

2. Введение

Всем привет, меня зовут Иван Пузыревский, я работаю в компании Яндекс. Последние лет шесть я занимался инфраструктурой хранения и обработки данных, сейчас перешел в продукт — в поиск путешествий, отелей и билетов. Так как я работал долгое время в инфраструктуре, то у меня накопилось довольно много опыта, как писать разные нагруженные приложения. Наша инфраструктура работает 24*7*365 каждый день нон-стоп, непрерывно на тысячах машин. Естественно, нужно писать код так, чтобы он работал надежно и производительно и решал задачи, которые перед нами ставит компания.

Сегодня мы с вами поговорим про асинхронность. Что такое асинхронность? Это несовпадение чего-либо с чем-либо во времени. Из этого описания вообще не понятно, про что я сегодня буду говорить. Чтобы как-то пояснить вопрос, мне нужен пример а-ля «Hello, world!». Асинхронность обычно возникает в контексте написания сетевых приложений, поэтому у меня будет сетевой аналог «Hello, world!». Это приложение ping-pong. Код выглядит таким образом:

Я создаю сокет, читаю оттуда строку, и проверяю — если это ping, то пишу в ответ pong. Очень просто и понятно. Что происходит, когда вы видите такой код на экране своего компьютера? Мы думаем об этом коде как о последовательности вот таких шагов:

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

С точки зрения реального физического времени все немного смещено.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Те, кто реально такой код писал и запускал, знают, что после шага read и после шага
write идет довольно заметный интервал времени, когда наша программа вроде бы ничего не делает с точки зрения нашего кода, но под капотом работает машинерия, которую мы называем «ввод-вывод».

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.
Во время ввода/вывода происходит обмен пакетами по сети и вся сопутствующая тяжелая низкоуровневая работа. Проведем мысленный эксперимент: возьмем такую одну программу, запустим на одном физическом процессоре и сделаем вид, что у нас нет никакой операционной системы, что получится? Процессор не может остановиться, он продолжает делать такты, не исполняя никаких инструкций, просто зря потребляя энергию.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

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

3. Основные понятия

3.1. Поток выполнения

Как мы можем подступиться к этой задаче? Давайте согласуем понятия. Я буду говорить «поток выполнения», имея в виду некоторую осмысленную последовательность элементарных операций или шагов. Осмысленность будет определяться контекстом, в котором я говорю о потоке выполнения. То есть если мы говорим про однопоточный алгоритм (Ахо-Корасик, поиск по графу), то сам этот алгоритм — уже есть поток выполнения. Он делает какие-то шаги для решения задачи.

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

3.2. Многозадачность и параллелизм

Краеугольный камень производительности — это умение сделать такой трюк: когда у меня есть один поток выполнения, который содержит в своей физической временной развертке пустоты, тогда заполнить эти пустоты чем-нибудь полезным — исполнить шаги других потоков выполнения.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Базы данных обычно обслуживают много клиентов одновременно. Если мы можем совместить работу над несколькими потоками выполнения в рамках одного потока выполнения более высокого уровня, то это называется многозадачность. То есть многозадачность — это когда я в рамках одного более крупного потока выполнения совершаю действия, которые подчинены решению более мелких задач.

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

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Важно не путать эти понятия, так как они относятся к разным категориям. Многозадачность — это свойство вашей программы, что она внутри структурирована как переменная работа над разными задачами. Параллелизм — это свойство среды исполнения, которое дает вам возможность за один такт времени работать над несколькими задачами.

Во многом асинхронный код и написание асинхронного кода — это написание многозадачного кода. Основная сложность связана с тем, как мне кодировать задачи и как ими управлять. Поэтому сегодня мы будем говорить именно об этом — о написании многозадачного кода.

4. Блокирование и ожидание

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Начнем с какого-нибудь простого примера. Вернемся к ping-pong:

Как мы уже обсудили, после строчек read и white поток выполнения засыпает, блокируется. Обычно мы так и говорим, «поток заблокирован».

Это значит, что поток выполнения дошел до такой точки, когда для дальнейшего его продолжения необходимо наступление какого-либо события. В частности, в случае нашего сетевого приложения нужно, чтобы поступили данные по сети или, наоборот, у нас освободился буфер для записи данных в сеть. События могут быть разные. Если мы говорим про временные аспекты, то можем ждать срабатывания таймера или завершения другого процесса. События здесь — некая абстрактная вещь, про них важно понимать, что их можно ожидать.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Когда мы пишем простой код, то неявно отдаем управление ожиданием событий уровню выше. В нашем случае — операционной системе. Она, как сущность более высокого уровня, отвечает за выбор, какая задача будет исполняться далее, и она же отвечает за отслеживание наступления событий.

Наш код, который мы пишем как разработчики, структурирован в это же время относительно работы над одной задачей. Фрагмент кода из примера занимается обработкой одного соединения: он из одного соединения читает ping и в одно соединение пишет pong.

Код понятный. Его можно прочитать и понять, что он делает, как он работает, какую задачу решает, какие инварианты в нем есть и так далее. При этом мы очень слабо управляем планированием задач в такой модели. Вообще, в операционных системах есть понятия приоритетов, но если вы писали системы мягкого реального времени, то знаете, что инструментов, доступных в Linux, не хватает для создания достаточно вменяемых систем реального времени.

Далее, операционная система — штука сложная, и переключение контекста из нашего приложения в ядро стоит единицы микросекунд, что при некотором несложном подсчете дает нам оценку на порядка 20-100 тысяч переключений контекста в секунду. Это значит, что если мы пишем веб-сервер, то за одну секунду можем обработать порядка 20 тысяч запросов, предполагая, что обработка запросов стоит в десять раз дороже, чем работа системы.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

4.1. Неблокирующее ожидание

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

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

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

Также стоит добавить, что можно прийти не к select/epoll, а к библиотеке типа libuv, у которой в API не будет никаких событий, но будет множество коллбэков. Интерфейс библиотеки будет говорить: «Дорогой друг, для чтения сокета предоставь коллбэк, который я позову, когда появятся данные».

Что поменялось по сравнению с нашим синхронным кодом в предыдущей главе? Код стал асинхронным. Это значит, что мы в приложение забрали логику по определению момента времени, когда отслеживается наступление событий. Явные вызовы select/epoll — это точки, где мы запрашиваем у операционной системы информацию о наступивших событиях. Также мы забрали в код своего приложения выбор, над какой задачей работать дальше.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

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

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

Зачем мы, разработчики на C++, которые пишут и решают конкретные прикладные задачи, можем захотеть притащить в свой код событийную модель? Если мы перетаскиваем в свой код работу над многими задачами и управление ими, то из-за отсутствия перехода в ядро и обратно, мы можем чуть быстрее работать и за единицу времени совершать больше полезных действий.

К чему это приводит с точки зрения кода, который мы пишем? Возьмем, к примеру, nginx — высокопроизводительный HTTP-сервер, очень распространенный. Если почитать его код, он построен по асинхронной модели. Код читать довольно сложно. Когда ты задаешься вопросом, что же конкретно происходит при обработке одного HTTP-запроса, то оказывается, что в коде есть очень много фрагментов, разнесенных по разным файлам, по разным углам кодовой базы. Каждый фрагмент совершает маленький объем работы в рамках обслуживания всего HTTP-запроса. К примеру:

Есть структура request, которая пробрасывается в обработчик наступивших событий, когда сокет сигнализирует о доступности на чтение или на запись. Дальше этот обработчик постоянно по ходу работы программы переключается в зависимости от того, в каком состоянии находится обработка запроса. Либо мы читаем заголовки, либо читаем тело запроса, либо спрашиваем у upstream данные — в общем, много разных состояний.

Такой код сложновато читать, потому что он, в своей сути, описан в терминах реакции на события. Мы находимся в таком-то состоянии и реагируем определенным образом на наступившие события. Не хватает целостной картины обо всем процессе обработки HTTP-запроса.

Другой вариант – который обычно часто используется в JavaScript — это построение кода на основе коллбеков, когда мы пробрасываем в интерфейсный вызов свой коллбэк, в котором обычно есть еще какой-нибудь вложенный коллбэк на наступление события и так далее.

Код опять сильно фрагментирован, нет понимания текущего состояния, как мы работаем над запросом. Через замыкания передается много информации, и нужно прикладывать умственные усилия, чтобы реконструировать логику обработки одного запроса.

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

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Почему? Представим, у меня есть простой кейс, например, я читаю файл и передаю его по сети. В неблокирущем варианте этому кейсу будет соответствовать такой линейный конечный автомат:

Теперь, допустим, я хочу к этому файлу добавить информацию из базы данных. Простой вариант:

Вроде как линейный код, но количество состояний увеличилось.

Дальше вы начинаете думать, что было бы неплохо распараллелить два шага — чтение из файла и из базы данных. Начинаются чудеса комбинаторики: вы находитесь в начальном состоянии, запрашиваете чтение файла и данные из базы данных. Дальше вы можете прийти либо в состояние, где есть данные из базы данных, но нет файла, либо наоборот — есть данные из файла, но нет из базы данных. Далее нужно перейти в состояние, когда у вас есть одно из двух. Опять же это два состояния. Потом нужно перейти в состояние, когда у вас есть оба ингредиента. Потом писать их в сокет и так далее.

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

5. Futures/Promises

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Чтобы решить проблему, нужно посмотреть на ситуацию проще.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Есть программа, в ней есть черные и красные кружочки. Наш поток выполнения – это черные кружочки; иногда они перемежаются красными, когда поток не может продолжать свою работу. Проблема в том, что для нашего черного потока выполнения нужно попасть в следующий черный кружочек, который будет неизвестно когда.

Проблема в том, что когда мы пишем код на языке программирования, мы объясняем компьютеру, что делать прямо сейчас. Компьютер — условно простая штука, которая ожидает инструкции, которые мы пишем на языке программирование. Она ожидает инструкции про следующий кружочек, и в нашем языке программирования не хватает средств, чтобы сказать: «В будущем, пожалуйста, когда наступит некоторое событие, сделай что-нибудь».

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

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

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

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

В уровне 1 мы открыли этот ящик Пандоры, и он привнес в код много switch, case, условий, ветвей, состояний. Хочется какого-то компромисса, чтобы код был относительно читаемый, но сохранял все преимущества уровня 1.

К счастью для нас, в 1988 году люди, занимающиеся распределенными системами, Барбара Лисков и Люба Ширира, осознали проблему, и пришли к необходимости лингвистических изменений. В язык программирования нужно добавить конструкции, позволяющие выражать темпоральные связи между событиями — в текущем моменте времени и в неопределенном моменте в будущем.

Это назвали Promises. Концепция классная, но она двадцать лет пылилась на полке. В последнее время она набирает интерес — к примеру, товарищи в Twitter, когда рефакторили свой код с Ruby on Rails на Scala, прониклись этой концепцией достаточно глубоко, и решили, что все сервисы будут суть функция, которая берет запрос и возвращает future на ответ. Вы можете прочитать статью Your Server as a Function. Очень стройная концепция, которая позволила им очень оперативно переструктурировать весь код.

Но это Scala, а что делать нам, С++ разработчикам?

Нам нужна некая абстракция, назовем её Future. Это контейнер для значения типа T cо следующей семантикой: прямо сейчас значение в контейнере может отсутствовать, но когда-то в будущем оно появится.

С помощью этого контейнера мы будем связывать те значения, что появятся в будущем, с теми, что есть в настоящем моменте. То есть мы, находясь в «сейчас», будем говорить, что нужно будет сделать в будущем. В дальнейшем в рассказе Future будем называть интерфейсом для «чтения», а Promise — для «записи». В других языках программирования именования могут быть другими; к примеру, в JavaScript, Promise — интерфейс для чтения и записи одновременно, а в Java – есть только Future.

Чтобы проиллюстрировать идею, я буду использовать модельную реализацию. Если вам нужен реальный код, который можно использовать в своей кодовой базе, то стоит посмотреть на boost::future (не std::future) — в нем есть большая часть того, о чем мы будем говорить.

5.1. Интерфейс Future & Promise

Это контейнер, значит, есть какое-то значение, о котором мы хотим контейнер спросить. В частности, узнать, есть ли в нем значение сейчас, достать его оттуда. И, раз значения могут появиться в будущем, было бы неплохо иметь возможность подписаться — предоставить некоторую функцию, которая будет вызвана, когда появится значения. Для выразительности добавим ещё две функции Then, о которых я буду говорить позже.

Интерфейс для записи. Через него также можно опросить контейнер, есть ли в нем значение или нет. Можем сказать контейнеру «запиши, пожалуйста, значение, которое у меня есть в руках».

5.2. Композиция вычислений

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

В чем крутость конструкции? Крутость начинается, когда вы пытаетесь написать комбинаторы для связывания ваших вычислений. Функция Then — это то, что позволяет делать комбинации такого рода.

Я могу скомбинировать мое обещание в будущем получить значение t и желание применить трансформацию f. Тогда я получу обещание в настоящем моменте времени, о том, что в будущем появится значение r.

Появится каким способом: когда будет значение t, то я применю к нему функцию, получу значение r и положу в заранее подготовленный контейнер. С точки зрения кода это выглядит так:

На диаграмме мы находимся в левом нижнем углу, в будущем когда-то появится значение типа t. Мы над ним в будущем позовем функцию f, и ещё дальше в будущем появится значение r, которое положим в контейнер. Верхняя дуга связывает обещание, которое даем в настоящем и то значение, которое мы получим.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

С точки зрения кода, в момент вызова Then случаются три шага:

Функция работает быстро и моментально связывает, что произойдет в настоящем и в будущем. Такой подход удобен тем, что позволяет прямо сейчас сконструировать конвейер обработки, не дожидаясь никаких событий, и выносит за скобки логику ожидания событий и вызова реакции на них.

Если обратить внимание на слайды, то можно заметить, что в предыдущих примерах я только подписываюсь на появление значения, но нигде не вызываю функции-обработчики. Чтобы конструкция заработала, нужны интерфейсы, чтобы вызывать функции-обработчики, переданные в Subscribe. Нужен поток или пул потоков в фоне, в которые вы сможете, при наступлении событий, запланировать функции-обработчики на исполнение. Они будут исполнены, и вся конструкция заведется.

5.3. Примеры

Мы это можем выразить таким сниппетом. Он утрированный, но что здесь важно: желание посчитать (2v+1) 2 с точки зрения исходного кода очень локально. Мы уже в этом сниппете кода написали, что хотим сделать во все последующие моменты времени и взглядом охватить происходящее.

С точки зрения времени исполнения, картинка не будет соответствовать написанному, но сейчас нас это беспокоить не должно. Мы хотим исправить проблему с понятностью кода.

Второй пример. Имеется несколько функций: первая считает ключ, по которому в БД хранятся секретные послания; вторая читает данные из БД по ключу; третья отправляет данные роверу на другой планете.

Можно скомбинировать все три шага в новую функцию — ExploreOuterSpace. С точки зрения кода она состоит просто из цепочки вызовов Then; логика работы функции — последовательная композиция действий — помещается на один экран, её просто понять и осознать. При этом все шаги исполнения будут во времени (скорее всего) разнесены. Темпоральный характер связывания вынесен за скобки.

5.4. Any-комбинатор

Приятный бонус: если применять конструкцию с Future в среде с параллелизмом, то можно выстроить ещё более интересные комбинаторы, которых нет в однопоточном последовательном коде. Например, можем сказать, что мы бы хотели дождаться появления любого из двух значений:

Мы создаем обещание, что в будущем посчитаем это Any-значение, и в подписке на два Future устраиваем гонку: кто первый успел, тот и молодец. То есть если в контейнере пусто, то кладем туда значение, которое появилось.

Это может понадобиться, к примеру, когда у нас есть две базы данных, и мы читаем из обеих, и смотрим, откуда быстрее пришел ответ. То есть пишем код вида «Отправить запрос в DB1, отправить запрос в DB2, и как только получили любой из ответов — делаем что-то ещё».

5.5. All-комбинатор

Симметрично можем устраивать барьеры. Если из двух баз данных мы читаем две существенно разные записи и хотим комбинировать их у себя локально, то можно один раз написать барьерную логику, создать контейнер, и заполнять его частями (пары T1 и T2), прописаться в обработчике T1 и T2 на появление значений, положить их соответствующие компоненты контейнера и завести счетчик, сколько шагов осталось.

Вспомним пример с nginx. Там отслеживание того, какие части обработки запросы уже закончены, был явный и привязанный к конкретному домену приложения. То есть в случае nginx были выделены стадии «разбираю заголовки», «читаю тело запроса», «пишу заголовки и ответа ответа» и так далее. В All-комбинаторе логика по отслеживанию завершенных шагов абстрагирована до подсчета того, сколько фрагментов уже закончило работу. Это позволяет схлопнуть сложность нашего приложения.

5.6. Адаптация обратного вызова

Третий плюс Future и Promises — они просто интегрируются с legacy-кодом, построенным на коллбеках. Можно элегантно подцепить существующие callback-ориентированные библиотеки в наш код, обернув их в несложную обертку, устроенную простым образом: мы создаем Future, который сразу же и возвращает, а в callback-функции заполняем Future.

Итого: мы получили простой и читаемый код, инструменты для композиции нашего кода и сохранили возможность реализовать исполнение фрагментов кода поверх Future неблокирующим образом.

6. Как сделать код читаемее с помощью корутин

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Иногда возникают ситуации, когда у нас возникает понятный, но нелинейный по данным код. Рассмотрим сниппет.

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

Вроде бы, связка читаема, но выглядит не совсем аккуратно. Что делать? Типичный подход — всё рефакторить, не пробрасывать отдельно request и payload, а завести абстрактный контекст — один аргумент, который всюду будем протаскивать сквозным образом.

Например, так в Java устроена библиотека Netty. Удобно, но тогда контекст становится общей тарелкой, куда разные фрагменты кода пишут или читают значения. Сложно понять, что в этом контексте реально заполнено и когда, и что там происходит.

Если бы мы писали синхронный код, то позвали бы GetRequest, QueryBackend, HandlePayload и потом Reply от двух аргументов, если бы не было Future.

Чтобы воплотить фантазии в реальность, нужен некий метод, который принимает Future и возвращает T — назовём его WaitFor.

Тогда мы реформируем код таким образом:

Почему стало проще: убрали обертку из Future, которую только что добавили. Код читаемый и удобный. Также теперь в нем явным образом размечены четыре точки, где мы ждем данных. Это точки разрыва нашего потока исполнения.

У нас появляется две опции. Мы в этих точках на уровне нашего приложения можем выбрать как именно мы будем связывать эти два разорванных фрагмента кода. Либо мы можем связывать а-ля уровнь 0, синхронно заблокировать наш тред через, к примеру, mutex+cvar внутри future. Либо можем поставить себе задачу сделать неблокирующее ожидание. Мы освободим квант времени в нашем приложении под другие задачи, а текущий поток исполнения заморозим.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

6.1. Корутины

Это функции, в которых есть множество точек входа и выхода. Если вспомнить картинку с черными и красными кружочками, то там, где черный меняется на красный, нам надо выйти, потому что есть какие-то события, которых нужно подождать. Но если мы прерываем корутину, нам нужно выйти куда-то.

В нашем случае каждая точка ожидания — «выход» в некую сущность более высокого уровня, которая решит, какой задачей заняться далее. На уровне операционной системы это планировщик задач. У нас будет свой планировщик. Модельная имплементация: boost::asio и boost::fiber.

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

6.2. Черновик реализации WaitFor

Есть разные инструменты, например, boost::context, который в сущности сводится к интерфейсу такого вида: есть структура, отвечающая за контекст; есть функция, которая принимает один контекст для сохранения текущего контекста и один контекст для загрузки следующего. В ассемблере для x86/64 типичная реализация выписывает все регистры на стек, сохраняет в структуре текущий указатель на стек, далее заменяет указатель на стек новым и восстанавливает регистры.

Пока можно думать об этой функции, как об управляемом в процессе исполнения goto: когда в коде говорите, что прыгните в другую точку, но она задается не статически в момент компиляции, а подбирается динамически.

Чтобы доработать конструкцию до рабочего состояния, нужно как-то оперировать запланированными задачами. Обычно планируемую задачу называют Fiber — логический поток выполнения. Мы её будем представлять как пару контекст+Future. Для того, чтобы привязывать все конструкции с ожиданием, будем хранить в каждом логическом потоке выполнения ту Future, которую он сейчас ждет.

Здесь Future будет либо пустой, если мы ничего не ждем, либо некий заполненный объект, если мы ожидаем этот объект. Планировщик устроен таким образом: основная функция Loop, которая содержит цикл основной работы программы, очередь логических потоков выполнения, которые нужно запланировать, указатель на текущий поток выполнения и контекст планировщика, в который будем переключаться.

Как будет выглядеть функция WaitFor?

Здесь важное наблюдение: для того, чтобы дождаться какого-то значения в будущем, нам не очень важен его тип, поэтому мы хотим работать с Future поверх void, и конкретный тип значения нас интересовать не будет. И нам придется только один раз реализовать логику по темпоральной привязке.

Конструкция Future игнорирует тип контейнера, использует его как абстрактный контейнер, который будет заполнен когда-то в будущем.

Тогда функция WaitFor будет выглядеть следующим образом: я говорю планировщику: «Я бы хотел в текущем Fiber дождаться появления Future», и в следующей инструкции моей текущей сопрограммы (потока выполнения) сразу из этого контейнера хочу извлечь значение.

Как планировщик будет обслуживать моё желание? Он запомнит, что текущий поток выполнения ожидает некоторый Future, и перейдет обратно в цикл планирования.

6.3. Как устроен цикл планирования

Раз мы что-то уже запустили, наверное, у нас была очередь, из которой мы извлекли какой-то готовый к планированию логический поток, и перепрыгнули в него. Когда мы сделали SwitchContext из нашего потока, возвращаемся в точку 2 — из логического потока выполнения возвращаемся в планировщик.

Что происходит дальше? Нам нужно понять, если поток выполнения, из которого мы вернулись, попросил нас дождаться появления Future, то тогда подпишемся на этот ожидаемый Future, чтобы, при появлении значения в будущем, мы могли обратно поставить наш логический поток выполнения в цикл планирования.

Чтобы не запутаться, можно посмотреть на два этих слайда. Ход исполнения программы будет устроен таким образом:

Когда я зову функцию WaitFor — перехожу в планировщик.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Планировщик с помощью Switch-контекста прыгает в основной цикл.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Дальше я подписываюсь на Future и в будущем (обозначено синей стрелкой), когда значение появится, я положу в очередь исполнения обратно мой логический поток. Тогда в какой-то следующей итерации я этот логический поток вытащу из очереди и перепрыгну обратно в мой Fiber.

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Вернувшись в WaitFor я смогу извлечь из Future значение, и там уже точно что-то будет, Future будет заполнен. Мы получим такую классную конструкцию:

Мы сможем написать код, который выглядит как синхронный, в нем явно размечены точки ожидания, они хорошо читаются. При этом сохранены возможности приложения влиять на планирование задач, а переключать контекст дешевле, чем это делает ядро.

6.4. Coroutine TS

Можно ли сделать лучше? В будущем — да. Когда будет Coroutine TS, можно будет убрать скобочки в коде и сказать, что WaitFor и CoroutineWait, который получается из CoroutineTS — это более-менее одинаковые сущности. Это явно размеченные точки ожидания, где нам нужно прерывать текущий поток выполнения и чего-то дождаться. Соответственно, можно будет реализовать Waiter и Co, которые будут делать всё то, что на предыдущих слайдах.

7. Что получили?

Давайте подведем итог. С помощью корутин мы получили механизм для написания читаемого внятного кода, похожего на синхронный, сохранили возможность улучшать производительность, реализуя всю сложную машинерию. Но заплатили за это тем, что реализовали свой планировщик из операционной системы, и нам нужно его поддерживать, отлаживать и так далее.

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

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

что такое асинхронный код блока. image loader. что такое асинхронный код блока фото. что такое асинхронный код блока-image loader. картинка что такое асинхронный код блока. картинка image loader. Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

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

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

Если вы автор nginx, у вас, к сожалению, тернистый путь, вам нужно явно работать с низкими уровнями, вписать этот сложный код. И если вы хотите максимально уменьшать накладные расходы, то вы срезаете все абстракции, в частности, не используете никаких future и promises.

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

Тогда вам будут удобны абстракции типа futures, promises и actors. Это может сэкономить время. При этом эти абстракции можно более глубоко интегрировать в язык за счет сопрограмм, как я постарался проиллюстрировать.

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

Минутка рекламы. 19-20 апреля в Москве пройдёт конференция C++ Russia 2019. На ней будут доклады похожей тематики, например, Grimm Rainer приедет с докладом «Concurrency and parallelism in C++17 and C++20/23», а Павел Новиков расскажет об асинхронной разработке на C++. Подробнее программу можно посмотреть на официальном сайте, там же можно приобрести билет. Обратите внимание, что если вы не сможете приехать на конференцию вживую, есть билеты на онлайн-трансляцию.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *