асинхронное программирование что это такое
Асинхронность в программировании
Авторизуйтесь
Асинхронность в программировании
Традиционно в программировании используют синхронное программирование — последовательное выполнение инструкций с синхронными системными вызовами, которые полностью блокируют поток выполнения, пока системная операция, например чтение с диска, не завершится. В качестве примера ниже написан echo-сервер:
При вызове методов read() и write() текущий поток исполнения будет прерван в ожидании ввода-вывода по сети. Причём большую часть времени программа будет просто ждать. В высоконагруженных системах чаще всего так и происходит — почти всё время программа чего-то ждёт: диска, СУБД, сети, UI, в общем, какого-то внешнего, независимого от самой программы события. В малонагруженных системах это можно решить созданием нового потока для каждого блокирующего действия. Пока один поток спит, другой работает.
Но что делать, когда пользователей очень много? Если создавать на каждого хотя бы один поток, то производительность такого сервера резко упадёт из-за того, что контекст исполнения потока постоянно сменяется. Также на каждый поток создаётся свой контекст исполнения, включая память для стека, которая имеет минимальный размер в 4 КБ. Эту проблему может решить асинхронное программирование.
Асинхронность
Асинхронность в программировании — выполнение процесса в неблокирующем режиме системного вызова, что позволяет потоку программы продолжить обработку. Реализовать асинхронное программирование можно несколькими способами, о которых вы узнаете ниже.
Callbacks
Для написания асинхронной программы можно использовать callback-функции (от англ. callback — обратный вызов) — функции, которые будут вызваны асинхронно каким-либо обработчиком событий после завершения задачи. Переписанный пример сервера на callback-функциях:
В результате мы получили асинхронную работу нескольких соединений в одном единственном потоке, который намного реже будет ждать. Эту асинхронность можно также распараллелить, чтобы получить полный профит от утилизации процессорного времени.
У такого подхода есть несколько проблем. Первую в шутку называют callback hell. Достаточно погуглить картинки на эту тему, чтобы понять, насколько это нечитаемо и некрасиво. В нашем примере всего две вложенные callback-функции, но их может быть намного больше.
Async/Await
Попробуем сделать асинхронный код так, чтобы он выглядел как синхронный. Для большего понимания немного поменяем задачу: теперь нам необходимо прочитать данные из СУБД и файла по ключу, переданному по сети, и отправить результат обратно по сети.
Пройдём по программе построчно:
Это быстрее, чем последовательное ожидание сначала БД, затем файла. Во многих реализациях производительность async / await лучше, чем у классических callback-функций, при этом такой код читается как синхронный.
Корутины
Описанный выше механизм называется сопрограммой. Часто можно услышать вариант «корутина» (от англ. coroutine — сопрограмма).
Далее будут описаны различные виды и способы организации сопрограмм.
Несколько точек входа
Для большего понимания рассмотрим код на языке Python:
Программа выведет всю последовательность чисел факториала с номерами от 0 до 41.
Stackful и Stackless
В зависимости от использования стека корутины делятся на stackful, где каждая из корутин имеет свой стек, и stackless, где все локальные переменные функции сохраняются в специальном объекте.
На рисунке ниже вызов async создаёт новый стек-фрейм и переключает исполнение потока на него. Это практически новый поток, только исполняться он будет асинхронно с основным.
yield в свою очередь возвращает обратно предыдущий стек-фрейм на исполнение, сохраняя ссылку на конец текущего в предыдущий стек.
Наличие собственного стека позволяет делать yield из вложенных вызовов функций, но такие вызовы сопровождаются полным созданием/сменой контекста исполнения программы, что медленней, чем stackless корутины.
Более производительными, но вместе с тем и более ограниченными, являются stackless корутины. Они не используют стек, и компилятор преобразует функцию, содержащую корутины, в конечный автомат без корутин. Например, код:
Будет преобразован в следующий псевдокод:
Симметричные и асимметричные
Корутины также делятся на симметричные и асимметричные.
В асимметричных корутинах нет глобального планировщика, и программист вместе с поддержкой компилятора сам выбирает, какую корутину и когда исполнять. Большинство реализаций корутин асимметричные.
Вывод
Асинхронное программирование является очень мощным инструментом для оптимизации высоконагруженных программ с частым ожиданием системы. Но, как и любую сложную технологию, её нельзя использовать только потому, что она есть. Необходимо всегда задавать себе вопрос: а нужна ли мне эта технология? Какую практическую пользу она мне даст? Иначе разработчики рискуют потратить очень много сил, времени и денег, не получив никакого профита.
Основные понятия асинхронного программирования
В этой статье мы бегло познакомимся с основными понятиями, связанными с асинхронным программированием и как они применяются в веб браузерах и JavaScript. Вы должны понять эти концепции, прежде чем приступать к другим статьям этого раздела.
Необходимые знания: | Базовая компьютерная грамотность, знакомство с основами JavaScript. |
---|---|
Цель: | Понять основные идеи асинхронного программирования, и как они проявляются в веб-браузерах и JavaScript. |
Что же такое Асинхронность?
Как правило, программный код выполняется последовательно, только одна конкретная операция происходит в данный момент времени. Если функция зависит от результата выполнения другой функции, то она должна дождаться пока нужная ей функция не завершит свою работу и не вернёт результат и до тех пор пока это не произойдёт, выполнение программы, по сути, будет остановлено с точки зрения пользователя.
Такое поведение удручает и говорит о неправильном использовании процессорного времени, к тому же современные компьютеры имеют процессоры с несколькими ядрами. Не нужно ничего ждать, вы можете передать следующую задачу свободному ядру процессора и когда она завершится, то сообщит вам об этом. Такой подход позволяет выполнять разные задачи одновременно, в этом и заключается задача асинхронности в программировании. Программная среда, которую вы используете (браузер в случае веб разработки), должна иметь возможность выполнять различного рода задачи асинхронно.
Блокировка кода
Асинхронные техники очень полезны, особенно при веб разработке. Когда ваше приложение запущено в браузере и выполняет свои задачи, не возвращая контроль окружению, браузер может подвисать. Это называется блокировка; браузер заблокирован и не может реагировать на действия пользователя и выполнять служебные.задачи, до тех пор пока веб приложение не освободит ресурсы процессора.
Давайте рассмотрим несколько примеров, которые покажут, что именно значит блокировка.
В нашем simple-sync.html примере (see it running live), добавим кнопке событие на клик, чтобы при нажатии на неё запускалась трудоёмкая операция (расчёт 10000000 дат, и вывод последней рассчитанной даты на консоль) после чего в DOM добавляется ещё один параграф:
Когда запустите этот пример, откройте JavaScript консоль и нажмите на кнопку — вы заметите, что параграф не появится на странице, до тех пор пока все даты не будут рассчитаны и результат последнего вычисления не будет выведен на консоль. Этот код выполняется в том порядке, в котором он написан в файле и самая последняя операция не будет запущена, пока не завершатся все операции перед ней.
Примечание: Предыдущий пример слишком не реальный. Вам никогда не понадобится считать столько дат в реальном приложении! Однако, он помогает вам понять основную идею.
В нашем следующем примере, simple-sync-ui-blocking.html (посмотреть пример), мы сделаем что-нибудь более реалистичное, с чем вы сможете столкнуться на реальной странице. Мы заблокируем действия пользователя отрисовкой страницы. В этом примере у нас две кнопки:
Если вы быстро нажмёте на первую кнопку и затем быстро кликните на вторую, вы увидите, что предупреждение не появится на странице, пока все круги не будут отрисованы. Первая операция блокирует выполнение следующей до тех пор пока не завершится сама.
Примечание: Хорошо, в приведённом некрасивом примере, мы получили эффект блокировки, который показывает общую проблему при разработке приложений, с которой все время приходится бороться разработчикам.
Почему так происходит? Потому что JavaScript, в общем случае, выполняет команды в одном потоке. Пришло время познакомиться с понятием потока.
Потоки
Под потоком, обычно, понимают одиночный процесс, который может использовать программа, для выполнения своих нужд. Каждый поток может выполнять только одну в текущий момент времени:
Каждая задача будет выполнена последовательно; только когда текущая задача завершится, следующая сможет начаться.
Как мы говорили выше, большинство компьютеров теперь имеют процессор с несколькими ядрами, т.е. могут выполнять несколько задач одновременно. Языки программирования, поддерживающие многопоточность, могут использовать несколько ядер, чтобы выполнять несколько задач одновременно:
JavaScript однопоточный
JavaScript, традиционно для скриптовых языков, однопоточный. Даже, если есть несколько ядер, вы можете использовать их только для выполнения задач в одном потоке, называемом основной поток. Наш пример выше, выполняется следующим образом:
В итоге, JavaScript получил несколько инструментов, которые могут помочь в решении подобных проблем. Web workers позволяют вам обработать некоторый JavaScript-код в отдельном потоке, который называется обработчик, таким образом вы можете запускать отдельные блоки JavaScript-кода одновременно. В основном, вы будете использовать воркеры, чтобы запустить ресурсоёмкий процесс, отдельно от основного потока, чтобы не блокировать действия пользователя.
Помня об этом, выполните наш следующий пример simple-sync-worker.html (посмотреть пример в действии), с открытой консолью. Это переписанный предыдущий пример, который теперь рассчитывает 10 миллионов дат в отдельном потоке обработчика. Теперь, когда вы нажимаете на кнопку, браузер может добавить новый элемент на страницу, до того как все даты будут посчитаны. Самая первая операция больше не блокирует выполнение следующей.
Асинхронный код
Воркеры полезный инструмент, но у них есть свои ограничения. Самое существенное, заключается в том, что они не имеют доступа к DOM — вы не можете использовать воркер для обновления UI. Мы не можем отрисовать миллион наших точек внутри воркера; он может только обработать большой объем информации.
Следующая проблема заключается в том, что даже если код запущенный в воркере ничего не блокирует, он в целом остаётся синхронным. Это проблема появляется, когда какой-то функции требуются результаты выполнения нескольких предыдущих функций. Рассмотрим следующую диаграмму потоков:
В этом примере, предположим Task A делает что-то вроде получения картинки с сервера а Task B затем делает что-нибудь с полученной картинкой, например, применяет к ней фильтр. Если запустить выполняться Task A и тут же попытаться выполнить Task B, то вы получите ошибку, поскольку картинка ещё не будет доступна.
Теперь, давайте предположим, что Task D использует результат выполнения обеих задач Task B и Task C. Если мы уверенны, что оба результата будут доступны одновременно, тогда не возникнет проблем, однако, часто это не так. Если Task D попытаться запустить, когда какого-то нужного ей результата ещё нет, выполнение закончится ошибкой.
Чтобы избежать подобных проблем, браузеры позволяют нам выполнять определённые операции асинхронно. Такие возможности, как Promises позволяют запустить некоторую операцию (например, получение картинки с сервера), и затем подождать пока операция не вернёт результат, перед тем как начать выполнение другой задачи:
Поскольку операция выполняется где-то отдельно, основной поток не блокируется, при выполнении асинхронных задач.
В следующей статье, мы покажем вам, как писать асинхронный код. Захватывает дух, неправда ли? Продолжайте читать!
Заключение
При проектировании современных программ все больше используется асинхронное программирование, чтобы программа имела возможность выполнять несколько операций в конкретный момент времени. Как только вы начнёте использовать новые, более мощные возможности API, вы обнаружите множество ситуаций, где решить нужную задачу можно только асинхронно. Раньше было сложно писать асинхронный код. До сих пор, нужно время, чтобы привыкнуть к такому подходу, но процесс стал намного легче. Далее, в этом разделе, мы будем глубже исследовать вопрос, когда же асинхронный код необходим и как спроектировать программу, чтобы избежать проблем, описанных выше.
Асинхронное программирование (полный курс)
Асинхронное программирование за последнее время стало не менее развитым направлением, чем классическое параллельное программирование, а в мире 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, мне удалось не написать ни слова о том, как же они работают. Если успел заинтересовать — читайте материалы ниже, а я обещаю написать еще.
Дополнительные материалы
Асинхронное программирование: концепция, реализация, примеры
Разбираемся, чем асинхронное программирование отличается от синхронного, зачем оно нужно, и как реализуется асинхронность в разных языках.
Компьютерные программы часто имеют дело с длительными процессами. Например, получают данные из базы или производят сложные вычисления. Пока выполняется одна операция, можно было бы завершить еще несколько. А бездействие приводит к снижению продуктивности и убыткам. Асинхронное программирование увеличивает эффективность, потому что не позволяет блокировать основной поток выполнения.
Тенденции
Асинхронность была всегда, но в последние годы этот стиль разработки стал особенно популярным. Все современные языки имеют инструменты для его реализации и постоянно улучшают их. Например, от событий и функций обратного вызова мы перешли к обещаниям. Также существует множество библиотек асинхронности, например, 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 попадет первый вернувшийся результат из трех.
Это лишь простые примеры использования асинхронных инструментов в разных языках. Чтобы писать эффективный и понятный код, необходимо познакомиться с ними поближе. Например, почитать про обещания можно здесь и здесь.