Асинхронное программирование (полный курс)
Асинхронное программирование за последнее время стало не менее развитым направлением, чем классическое параллельное программирование, а в мире JavaScript, как в браузерах, так и в Node.js, понимание его приемов заняло одно из центральных мест в формировании мировоззрения разработчиков. Предлагаю вашему вниманию целостный и наиболее полный курс с объяснением всех широко распространенных методов асинхронного программирования, адаптеров между ними и вспомогательных проемов. Сейчас он состоит из 23 лекций, 3 докладов и 28 репозиториев с множеством примеров кода на github. Всего около 17 часов видео: ссылка на плейлист.
Пояснения к схеме
На схеме (выше) показаны связи между разными способами работы с асинхронностью. Цветные блоки относятся к асинхронному программированию, а ч/б показаны методы параллельного программирования (семафоры, мьютексы, барьеры и т.д.) и сети петри, которые, как и асинхронное программирование и модель акторов, являются разными подходами к реализации параллельных вычислений (они даны на схеме только чтоб точнее определить место асинхронного программирования). Модель акторов связана с асинхронным программированием потому, что реализация акторов без многопоточности тоже имеет право на существование и служит для структурирования асинхронного кода. Пунктирными линиями события и конкурентная очередь связаны с колбеками потому, что эти абстракции базируются на колбеках, но все же формируют качественно новые подходы.
Темы лекций
Под каждым видео есть ссылки на репозитории с примерами кода, которые разбираются в видео. Я постарался показать, что не нужно сводить все к одной абстракции асинхронности. Универсального подхода к асинхронности не существует, а для каждого случая можно подобрать те методы, которые позволят писать код более естественно для этой конкретной задачи. Конечно же, этот курс будут дополняться и я прошу всех предлагать новые темы и контрибьютить в примеры кода. Основная задача курса — это показать как строить абстракции асинхронности изнутри, а не просто научить ими пользоваться. Практически все абстракции не берутся из библиотек, а даны в самой простой их реализации и пошагово разобрана их работа.
Асинхронное программирование: концепция Deferred
Асинхронная концепция программирования заключается в том, что результат выполнения функции доступен не сразу же, а через некоторое время в виде некоторого асинхронного (нарушающего обычный порядок выполнения) вызова. Зачем такое может быть полезно? Рассмотрим несколько примеров.
Первый пример — сетевой сервер, веб-приложение. Чаще всего как таковых вычислений на процессоре такие приложения не выполняют. Большая часть времени (реального, не процессорного) тратится на ввод-вывод: чтение запроса от клиента, обращение к диску за данными, сетевые обращение к другим подсистемам (БД, кэширующие сервера, RPC и т.п.), запись ответа клиенту. Во время этих операций ввода-вывода процессор простаивает, его можно загрузить обработкой запросов других клиентов. Возможны различные способы решить эту задачу: отдельный процесс на каждое соединение (Apache mpm_prefork, PostgreSQL, PHP FastCGI), отдельный поток (нить) на каждое соединение или комбинированный вариант процесс/нить (Apache mpm_worker, MySQL). Подход с использованием процессов или нитей перекладывает мультиплексирование процессора между обрабатываемыми соединениями на ОС, при этом расходуется относительно много ресурсов (память, переключения контекста и т.п.), такой вариант не подходит для обработки большого количества одновременных соединений, но идеален для ситуации, когда объем вычислений достаточно высок (например, в СУБД). К плюсам модели нитей и процессов можно добавить потенциальное использование всех доступных процессоров в многопроцессорной архитектуре.
Альтернативой является использование однопоточной модели с использованием примитивов асинхронного ввода-вывода, предоставляемых ОС (select, poll, и т.п.). При этом объем ресурсов на каждое новое обслуживаемое соединение не такой большой (новый сокет, какие-то структуры в памяти приложения). Однако программирование существенно усложняется, т.к. данные из сетевых сокетов поступают некоторыми “отрывками”, причем за один цикл обработки данные поступают от разных соединений, находящихся в разных состояниях, часть соединений могут быть входящими от клиентов, часть — исходящими к внешним ресурсам (БД, другой сервер и т.п.). Для упрощения разработки используются различные концепции: callback, конечные автоматы и другие. Примеры сетевых серверов, использующих асинхронный ввод-вывод: nginx, lighttpd, HAProxy, pgBouncer, и т.д. Именно при такой однопоточной модели возникает необходимость в асинхронном программировании. Например, мы хотим выполнить запрос в БД. С точки зрения программы выполнение запроса — это сетевой ввод-вывод: соединение с сервером, отправка запроса, ожидание ответа, чтение ответа сервера БД. Поэтому если мы вызываем функцию “выполнить запрос БД”, то она сразу вернуть результат не сможет (иначе она должна была бы заблокироваться), а вернет лишь нечто, что позволит впоследствие получить результат запроса или, возможно, ошибку (нет соединения с сервером, некорректный запрос и т.п.) Этим возвращаемым значением удобно сделать именно Deferred.
Второй пример связан с разработкой обычных десктопных приложений. Предположим, мы решили сделать аналог Miranda (QIP, MDC, …), то есть свой мессенджер. В интерфейсе программы есть контакт-лист, где можно удалить контакт. Когда пользователь выбирает это действие, он ожидает что контакт исчезнет на экране и что он действительно удалится из контакт-листа. На самом деле операция удаления из серверного контакт-листа опирается на сетевое взаимодействие с сервером, при этом пользовательский интерфейс не должен быть заблокирован на время выполнения этой операции, поэтому в любом случае после выполнения операции потребуется некоторое асинхронное взаимодействие с результатом операции. Можно использовать механизм сигналов-слотов, callback’ов или что-то еще, но лучше всего подойдет Deferred: операция удаления из контакт-листа возвращает Deferred, в котором обратно придет либо положительный результат (всё хорошо), либо исключение (точная ошибка, которую надо сообщить пользователю): в случае ошибки контакт надо восстановить контакт в контакт-листе.
Примеры можно приводить долго и много, теперь о том, что же такое Deferred. Deferred — это сердце framework’а асинхронного сетевого программирования Twisted в Python. Это простая и стройная концепция, которая позволяет перевести синхронное программирование в асинхронный код, не изобретая велосипед для каждой ситуации и обеспечивая высокое качества кода. Deferred — это просто возвращаемый результат функции, когда этот результат неизвестен (не был получен, будет получен в другой нити и т.п.) Что мы можем сделать с Deferred? Мы можем “подвеситься” в цепочку обработчиков, которые будут вызваны, когда результат будет получен. При этом Deferred может нести не только положительный результат выполнения, но и исключения, сгенерированные функцией или обработчиками, есть возможность исключения обработать, перевыкинуть и т.д. Фактически, для синхронного кода есть более-менее однозначная параллель в терминах Deferred. Для эффективной разработки с Deferred оказываются полезными такие возможности языка программирования, как замыкания, лямбда-функци.
Приведем пример синхронного кода и его альтернативу в терминах Deferred:
В асинхронном варианте с Deferred он был бы записан следующим образом:
На практике обычно мы возвращаем Deferred из функций, которые получают Deferred в процессе своей работы, навешиваем большое количество обработчиков, обрабатываем исключения, некоторые исключения возвращаем через Deferred (выбрасываем наверх). В качестве более сложного примера приведем код в асинхронном варианте для примера атомарного счетчика из статьи про структуры данных в memcached, здесь мы предполагаем, что доступ к memcached как сетевому сервису идет через Deferred, т.е. методы класса Memcache возвращают Deferred (который вернет либо результат операции, либо ошибку):
Приведенный выше код можно написать “короче”, объединяя часто используемые операции, например:
В одной статье не описать и части того, что хотелось бы сказать о Deferred, мне удалось не написать ни слова о том, как же они работают. Если успел заинтересовать — читайте материалы ниже, а я обещаю написать еще.
Дополнительные материалы
Асинхронность: назад в будущее
Асинхронность… Услышав это слово, у программистов начинают блестеть глаза, дыхание становится поверхностным, руки начинают трястись, голос — заикаться, мозг начинает рисовать многочисленные уровни абстракции… У менеджеров округляются глаза, звуки становятся нечленораздельными, руки сжимаются в кулаки, а голос переходит на обертона… Единственное, что их объединяет — это учащенный пульс. Только причины этого различны: программисты рвутся в бой, а менеджеры пытаются заглянуть в хрустальный шар и осознать риски, начинают судорожно придумывать причины увеличения сроков в разы… И уже потом, когда большая часть кода написана, программисты начинают осознавать и познавать всю горечь асинхронности, проводя бесконечные ночи в дебаггере, отчаянно пытаясь понять, что же все-таки происходит…
Именно такую картину рисует мое воспаленное воображение при слове “асинхронность”. Конечно, все это слишком эмоционально и не всегда правда. Ведь так. Возможны варианты. Некоторые скажут, что “при правильном подходе все будет работать хорошо”. Однако это можно сказать всегда и везде при всяком удобном и не удобном случае. Но лучше от этого не становится, баги не исправляются, а бессонница не проходит.
Так что же такое асинхронность? Почему она так привлекательна? А главное: что с ней не так?
Введение
Асинхронность на текущий момент является достаточно популярной темой. Достаточно просмотреть последние статьи на хабре, чтобы в этом убедиться. Тут тебе и обзор различных библиотек, и использования языка Go, и всякие асинхронные фреймворки на JS, и много чего другого.
Обычно асинхронность используется для сетевого программирования: всякие сокеты-шмокеты, читатели-писатели и прочие акцепторы. Но бывают еще забавные и интересные события, особенно в UI. Здесь я буду рассматривать исключительно сетевое использование. Однако, как будет показано в следующей статье, подход можно расширять и углублять в неведомые дали.
Чтобы быть совсем уж конкретным, будем писать простой HTTP сервер, который на некий любой запрос посылает некий стандартный ответ. Это чтоб не писать парсер, т.к. к теме асинхронности он имеет ровно такое же отношение, как положение звезд к характеру человека (см. астрологию).
Синхронный однопоточный сервер
Хм. Синхронный? А при чем тут синхронный, спросит внимательный читатель, открыв статью про асинхронность. Ну, во-первых, надо же с чего-то начать. С чего-то простого. А во-вторых… Короче, я автор, поэтому будет так. А потом и сами узнаете, зачем.
Итак, описание сокета и акцептора:
Ничего лишнего, просто сервер. Socket позволяет писать и читать, в том числе до определенных символов ( readUntil ). Acceptor слушает указанный порт и принимает соединения.
Реализация всего этого хозяйства приведена ниже:
Давайте теперь напишем долгожданный сервер. Вот он:
Синхронный многопоточный сервер
Теперь можно и сервер написать:
Казалось бы, все хорошо, но не тут то было: на реальных задачах под нагрузкой это дело ложится быстро и потом не отжимается. Поэтому умные дядьки подумали, подумали, и решили использовать асинхронность.
Асинхронный сервер
В чем проблема предыдущего подхода? А в том, что потоки вместо реальной работы большую часть времени ожидают на событиях из сети, отжирая ресурсы. Хочется как-то более эффективно использовать потоки для выполнения полезной работы.
Ну что ж, вроде пока ничего страшного нет.
Помимо этого необходимо написать аналог go и диспетчеризацию:
Здесь указывается обработчик, который будет запускаться асинхронно в пуле потоков и, собственно, создание пула потоков с последующей диспетчеризацией.
Вот как выглядит реализация:
Здесь мы используем sync::go для создания потоков из синхронного подхода.
Вот такая простыня. С каждым новым вызовом растет вложенность лямбд. Обычно, конечно, такое через лямбды не пишут, т.к. есть сложности с зацикливанием: в лямбду необходимо пробрасывать саму себя, чтобы внутри самой себя позвать саму себя. Но тем не менее, читабельность кода будет примерно одинаковая, т.е. одинаково плохая при сравнении с синхронным кодом.
Сопрограммы
Итак, чего же нам всем хочется? Счастья, здоровья, денег мешок. А хочется простого: использовать плюсы асинхронного и синхронного подходов одновременно, т.е. чтобы и производительность была как у асинхронного, и простота как у синхронного.
На бумаге звучит замечательно. Возможно ли это? Для ответа на вопрос нам понадобится небольшое введение в сопрограммы.
Вот что такое обычные процедуры? Находимся мы, значит, в каком-то месте исполнения и тут раз, и позвали процедуру. Для вызова сначала запоминается текущее место для возврата, затем зовется процедура, она исполняется, завершается и возвращает управление в то место, откуда была позвана. А сопрограмма — это то же самое, только другое: она тоже возвращает управление в то место, откуда была позвана, но при этом она не завершается, а останавливается в некотором месте, с которого дальше продолжает работать при повторном запуске. Т.е. получается эдакий пинг-понг: вызывающий бросает мячик, сопрограмма ловит его, перебегает в другое место, бросает обратно, вызывающий тоже что-то делает (перебегает) и снова бросает в предыдущее место уже сопрограммы. И так происходит до тех пор, пока сопрограмма не завершится. В целом можно сказать, что процедура — это частный случай сопрограммы.
Как теперь это можно использовать для наших асинхронных задач? Ну тут наводит на мысль то, что сопрограмма сохраняет некий контекст исполнения, что для асинхронности крайне важно. Именно это и буду я использовать: если сопрограмме потребуется выполнить асинхронную операцию, то я просто вызову асинхронный метод и выйду из сопрограммы. А обработчик по завершению асинхронной операции просто продолжит исполнение нашей сопрограммы с места последнего вызова той самой асинхронной операции. Т.е. вся грязная работа по сохранению контекста ложится на плечи реализации сопрограмм.
И вот тут как раз и начинаются проблемы. Дело в том, что поддержка сопрограмм на стороне языков и процессоров — дела давно минувших дней. Для реализации переключения контекстов исполнения сегодня необходимо проделать множество операций: сохранить состояния регистров, переключить стек и заполнить некоторые служебные поля для корректной работы среды исполнения (например, для исключений, TLS и др.). Более того, реализация зависит не только от архитектуры процессора, но еще и от компилятора и операционной системы. Звучит как последний гвоздь в крышку гроба…
Реализация сопрограмм
Итак, для наших целей напишем свои сопрограммы. Интерфейс будет такой:
Вот такой нехитрый интерфейс. Ну и сразу вариант использования:
Должен выдать на экран:
Начнем с метода start :
Здесь boost::context::make_fcontext создает нам контекст и передает в качестве стартовой функции статический метод starterWrapper0 :
Отмечу один интересный момент: если не сохранить обработчик внутри сопрограммы (до его вызова), то при последующем возврате программа может благополучно упасть. Это связано с тем, что, вообще говоря, обработчик хранит в себе некоторое состояние, которое может быть разрушено в какой-то момент.
Теперь осталось рассмотреть остальные функции:
Synca: async наоборот
И все было бы хорошо, если бы не пресловутая многопоточность. Как это всегда бывает, она вносит некоторую турбулентность, поэтому приведенный выше подход не будет работать должным образом, что наглядно иллюстрирует следующая диаграмма:
Отличие состоит в том, что мы запускаем шедулинг не в сопрограмме, а вне ее, что исключает возможность, описанную выше. При этом продолжение сопрограммы может случиться в другом потоке, что является вполне нормальным поведением, для этого сопрограммы и предназначены, чтобы иметь возможность тусовать их туда-сюда, сохраняя при этом контекст исполнения.
Реализация
Начнем с реализации функции go :
Действия простые: смотрим, есть ли что-то для обработки. Если есть — то выполняем, нет — тогда сопрограмма закончила свою работу и ее можно удалить.
Остальные функции реализуются аналогично:
Использование
Перейдем к использованию нашего функционала. Тут все гораздо проще и изящнее:
Но отличие в реализации носит принципиальный характер: получившийся код использует асинхронное сетевое взаимодействие, а значит является гораздо более эффективной реализацией. Собственно на этом наша цель достигнута: сделать симбиоз синхронного и асинхронного подходов, взяв из них самое лучшее, т.е. простоту синхронного и производительность асинхронного.
Улучшение
Опишу некоторое улучшение для процесса принятия сокетов. Часто, после принятия происходит разветвление исполнения: тот, кто принимал, будет продолжает принимать, а новый сокет будет обрабатываться в отдельном контексте исполнения. Поэтому создадим новый метод goAccept :
И тогда наш сервер перепишется в виде:
Что гораздо проще для понимания и использования.
Вопрос 1. А что с производительностью?
Действительно, отличие от чисто асинхронного подхода в том, что тут возникают дополнительные накладные расходы на создание/переключение контекстов и смежной атрибутики.
Тем не менее видно, что не смотря на наличие дополнительного переключения контекстов, а также пробрасыванием исключений вместо кодов возврата (исключение генерится каждый раз при закрытии сокета, т.е. каждый раз на новом запросе) накладные расходы пренебрежимо малы. А если еще добавить код, который бы честно парсил HTTP сообщение, а также код, который бы не менее честно обрабатывал запросы и делал что-нибудь важное и нужное, то можно заявить смело, что отличие в производительности не будет вообще.
Вопрос 2. Ну допустим. А можно ли таким способом решать более сложные асинхронные задачи?
Теорема. Любую асинхронную задачу можно решить с помощью сопрограмм.
Доказательство.
Вначале возьмем функцию, которая использует асинхронные вызовы. Любую функцию можно превратить в сопрограмму, т.к. функция является частным случаем сопрограммы. Далее возьмем какой-либо асинхронный вызов в такой преобразованной сопрограмме. Такой вызов можно представить в следующем виде:
Рассмотрим случай, когда у нас отсутствует код после вызова:
Такой код с точки зрения сопрограммы эквивалентен следующему:
Теперь осталось рассмотреть более общий случай, когда у нас присутствует код после асинхронного вызова. Такой код эквивалентен:
Т.е. на один асинхронный вызов стало меньше. Применяя такой подход к каждому асинхронному вызову функции и к каждой функции мы перепишем весь код на сопрограммах. Ч.т.д.
Выводы
Асинхронность стремительным домкратом врывается в нашу программистскую жизнь. Сложности, которые возникают при написании кода, способны привести в дрожь даже самых ярых и закаленных экспертов. Однако, не стоит списывать со счетов старый добрый синхронный подход: в умелых руках асинхронность превращается в элегантные сопрограммы.
В следующей статье будет рассмотрен гораздо более сложный пример, который раскроет всю мощь и потенциал сопрограмм!
Асинхронное программирование: концепция, реализация, примеры
Разбираемся, чем асинхронное программирование отличается от синхронного, зачем оно нужно, и как реализуется асинхронность в разных языках.
Компьютерные программы часто имеют дело с длительными процессами. Например, получают данные из базы или производят сложные вычисления. Пока выполняется одна операция, можно было бы завершить еще несколько. А бездействие приводит к снижению продуктивности и убыткам. Асинхронное программирование увеличивает эффективность, потому что не позволяет блокировать основной поток выполнения.
Тенденции
Асинхронность была всегда, но в последние годы этот стиль разработки стал особенно популярным. Все современные языки имеют инструменты для его реализации и постоянно улучшают их. Например, от событий и функций обратного вызова мы перешли к обещаниям. Также существует множество библиотек асинхронности, например, ReactiveX, которая работает в Java, C#, Swift, JavaScript и ряде других языков.
В мире, где никто не любит ждать, просто нельзя писать код синхронно! Чтобы не отставать от современных тенденций, нужно освоить асинхронное программирование.
Человек в синхронном мире
Один занятой молодой человек запланировал на вечер свидание. Он очень хочет, чтобы все прошло идеально, а для этого нужно сделать несколько дел:
Без торта, букета, костюма и стопки разобранных бумаг, свидание точно не состоится.
Молодой человек живет в синхронном мире. Это значит, что он не может приступить к следующему делу, пока не закончится предыдущее.
Прежде всего, нужно отправить запрос на торт, так как приготовление занимает несколько часов. Он звонит маме, и она тут же начинает замешивать тесто. К вечеру торт несомненно будет готов. Однако молодой человек не успеет сделать остальные дела, и свидание не состоится. Дело в том, что все это время он провел с трубкой у уха, ожидая подтверждения о завершении запроса. Бессердечный синхронный мир не позволил ему поработать и купить букет.
Решить проблему могло бы асинхронное программирование. С его помощью блокирующий процесс маминой готовки можно убрать из потока приготовления к свиданию.
В асинхронном мире человек не зависит от торта. Он просит маму перезвонить, а сам едет за парадным костюмом в химчистку. Когда выложена последняя вишенка, мама запускает событие «Торт готов». Нарядный молодой человек хватает букет и бежит на свидание.
Асинхронное программирование
В синхронном коде каждая операция ожидает окончания предыдущей. Поэтому вся программа может зависнуть, если одна из команд выполняется очень долго.
Асинхронный код убирает блокирующую операцию из основного потока программы, так что она продолжает выполняться, но где-то в другом месте, а обработчик может идти дальше. Проще говоря, главный «процесс» ставит задачу и передает ее другому независимому «процессу».
Запрос данных
Асинхронное программирование успешно решает множество задач. Одна из самых важных – доступность интерфейса пользователя.
Возьмем для примера приложение, которое подбирает фильм по указанным критериям. После того как пользователь выбрал параметры, программа отправляет запрос на сервер. А там происходит подбор подходящих картин.
Обработка может длиться довольно долго. Если приложение работает синхронно, то пользователь не сможет взаимодействовать со страницей, пока не придет результат. Он не сможет даже скроллить!
Асинхронный код позволяет скрыть от пользователя эти неприятные эффекты и сохранить подвижность страницы. После того как данные загрузятся, программа выведет их на экран.
В этом случае главный поток выполнения разделяется на две ветви. Одна из них продолжает заниматься интерфейсом, а другая выполняет запрос.
Завершение асинхронной операции
Тут возникает проблема. Когда запрос завершится в дополнительной ветви, как об этом узнает главная? Как вернуть полученное значение в основной поток, если это необходимо? Для этого существуют события и механизм обратного вызова.
Если запрос выполняется асинхронно, то он может оповестить всех желающих о своем окончании. Программа подписывается на это сообщение и регистрирует для него обработчик. Когда придет время, запрос создаст событие и уведомит подписчиков.
Обработчик продолжает выполнять последующий код, пока не получит сообщение. Тогда он прервется и обработает его.
Асинхронных операций в программе может быть несколько. Чтобы разобраться с многочисленными событиями, существует специальная очередь. Она работает по принципу «первый пришел – первый ушел».
Чтобы детальнее изучить механизм обратного вызова, обратимся к Node.JS. В сердце этой платформы лежит библиотека LibUV. Она написана на языке C и способна наладить контакт с различными операционными системами. В Windows, Linux или MacOS библиотека чувствует себя как рыба в воде.
LibUV берет на себя фундаментальную задачу управления операциями ввода-вывода в Node.JS. При этом взаимодействие с программистом происходит через нескольких посредников:
Любой скрипт в однопоточной Node.JS запускается в режиме цикла. Это значит, что выполнение синхронного JavaScript-кода постоянно чередуется с асинхронными событиями, например, обработкой ввода-выводы или таймерами. Пока есть, что обрабатывать, этот цикл не остановится.
В глубины коллбэков
Запустим простой сервер и на его примере проследим, как происходит передача управления.
Все это происходит в глобальном контексте выполнения кода.
Теперь ни одно событие порта не ускользнет от внимания LibUV. А сейчас библиотека просто отправляет сообщение о том, что все прошло успешно. Управление передается сначала Node.JS, а затем JavaScript-коду.
Прежде чем свернуть работу, Node.JS спросит у LibUV, не осталось ли внутренних наблюдателей. Если следить больше не за чем, то цикл остановится. Однако, в нашем случае один наблюдатель все же завалялся. Поэтому программа не завершает работу, а просто засыпает.
Сон длится ровно до того момента, как операционная система просигнализирует о присоединении к порту. Сработает наблюдатель LibUV, и после некоторых обработок сигнал попадет в Node.JS и JavaScript-код в виде события request на объекте сервера. JavaScript в ответ запускает функцию обратного вызова.
Функция, обрабатывающая запрос, запускается из глобального контекста. Именно в нем находился интерпретатор после старта сервера. Когда обработка завершится, он вновь вернется сюда и просигнализирует о том, что программу можно заканчивать. LibUV снова пойдет проверять своих наблюдателей. Цикл повторится.
Проблемы обратных вызовов
Обработчик события сам по себе может блокировать поток выполнения кода. Например, если внутри него синхронно выполняются сложные операции. Это возвращает нас к проблеме ожидания и зависания программы.
Чтобы избежать блокировки, можно сделать еще один обратный вызов. На самом деле, технически уровней вложенности может быть сколько угодно. Однако в большом количестве функций легко запутаться. Подобные конструкции называются адом обратных вызовов и являются плохим стилем кода.
Общая схема программы:
Другие решения
События выполнения и обратные вызовы – это классическая схема асинхронной модели. Так она реализована в большинстве языков. Однако у нее есть ряд недостатков.
Сейчас существуют более удобные инструменты для работы с асинхронностью. Их можно разделить на две группы.
Первая из них возвращает «обещания». Сюда относятся deferred, promises и futures.
Вторая реализует асинхронность вычислений. Это конструкции с ключевыми словами async/await. Впервые эта архитектура возникла в C#, но ее преимущества быстро оценили в других языках.
Немного терминов
Когда речь заходит об асинхронности, всплывают еще три близких понятия. Это конкурентность (concurrency), параллелизм (parallel execution) и многопоточность (multithreading). Все они связаны с одновременным выполнением задач, однако это не одно и то же.
Конкурентность
Понятие конкурентного исполнения самое общее. Оно буквально означает, что множество задач решаются в одно время. Можно сказать, что в программе есть несколько логических потоков – по одному на каждую задачу.
При этом потоки могут физически выполняться одновременно, но это не обязательно.
Задачи при этом не связаны друг с другом. Следовательно, не имеет значения, какая из них завершится раньше, а какая позже.
Параллелизм
Параллельное исполнение обычно используется для разделения одной задачи на части для ускорения вычислений.
Например, нужно сделать цветное изображение черно-белым. Обработка верхней половины не отличается от обработки нижней. Следовательно, можно разделить эту задачу на две части и раздать их разным потокам, чтобы ускорить выполнение в два раза.
Наличие двух физических потоков здесь принципиально важно, так как на компьютере с одним вычислительным устройством (процессорным ядром) такой прием провести невозможно.
Многопоточность
Здесь поток является абстракцией, под которой может скрываться и отдельное ядро процессора, и тред ОС. Некоторые языки даже имеют собственные объекты потоков. Таким образом, эта концепция может иметь принципиально разную реализацию.
Асинхронность
Идея асинхронного выполнения заключается в том, что начало и конец одной операции происходят в разное время в разных частях кода. Чтобы получить результат, необходимо подождать, причем время ожидания непредсказуемо.
Шаблоны асинхронности
Можно выделить три самые популярные схемы асинхронных запросов. Рассмотрим их реализацию с помощью «обещаний» (JavaScript) и операторов async-await (C#).
Для демонстрации потребуются тестовые функции, которые имитируют возвращение нужных объектов с задержкой.
Последовательное выполнение
Используется для связанных задач, которые нужно запускать друг за другом. Например, первый запрос получает названия фильмов, а второй – информацию о них.
Параллельное выполнение
Применяется для решения независимых задач, когда важно, чтобы выполнились все запросы. Например, данные веб-страницы грузятся с трех серверов, а после этого начинается рендеринг.
Параметр results – это массив, в котором содержатся результаты всех трех выполненных операций.
Метод WaitAll класса Task собирает результаты трех запросов вместе.
Конкурентное выполнение
Используется для решения независимых задач, когда важно, чтобы выполнился хотя бы один запрос. Например, отправка идентичных запросов на разные сервера.
В параметр result попадет первый вернувшийся результат из трех.
Это лишь простые примеры использования асинхронных инструментов в разных языках. Чтобы писать эффективный и понятный код, необходимо познакомиться с ними поближе. Например, почитать про обещания можно здесь и здесь.
















