Расширяя предыдущий блог цикла событий с использованием libuv (https://medium.com/@jain.sm/non-blocking-io-using-libuv-1790b8fdbeff), мы в этом блоге пытаемся понять, как происходит событие loop на самом деле работает в таких реализациях, как nodejs.

Прежде всего, nodejs - это в основном комбинация движка v8 (который используется для JIT-компиляции кода js в машинный код) и libuv, который обеспечивает цикл событий и возможности неблокирующего ввода-вывода для nodejs.

Цикл событий отвечает за поэтапное опустошение очередей событий. Существует 4 типа очередей событий, обрабатываемых потоком цикла событий libuv.

1. Таймеры - таймеры устанавливаются с помощью setTimeout и setInterval. Таймеры с истекшим сроком действия по сравнению с текущими обрабатываются циклом событий. Это реализовано как minheap.

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

3. Немедленная очередь - здесь обрабатываются все обратные вызовы, зарегистрированные через setImmediate.

4. Очередь CloseHandlers - здесь обрабатываются все обработчики закрытия.

Кроме того, этот узел добавляет свои собственные очереди для обработки потоком libuv. Это может заставить задуматься, как эти очереди становятся доступными для libuv. Мы рассмотрим этот аспект того, как осуществляется взаимодействие между nodejs и c ++ в обоих направлениях. Итак, очереди nodejs

1. Очередь NextTick - это обратные вызовы, добавленные с помощью метода process.NextTick.

2. Очередь MicroTasks - эти обратные вызовы в качестве примера включают собственные (ECMA) обещания.

Итак, теперь у нас есть цикл событий (Node использует цикл по умолчанию libuv) и эти очереди. Узел имеет определенный порядок обработки этих очередей, которые мы можем назвать фазами.

На каждой итерации он начинается с

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

2. Затем он обрабатывает очередь событий ввода-вывода для обработки всех обратных вызовов для событий завершения ввода-вывода. На этом этапе libuv берет файловые системы, готовые к обработке данных (например, raed / write on сокеты), и выполняет обратные вызовы, которые для них зарегистрированы. Libuv взаимодействует с базовыми механизмами ОС, такими как epoll, kqueue, select или IOCP.

3. Следующий этап завершается, если регистрируются какие-либо обратные вызовы с использованием setImmediate.

4. На последнем этапе рассматриваются все обратные вызовы обработчиков закрытия.

Теперь между всеми вышеуказанными этапами он вызывает определенные очереди Nodejs (очереди nextTick и MicroTasks). Между этими двумя очередями очередь nextTick имеет более высокий приоритет, чем микрозадачи. Таким образом, в основном nextTicks обрабатываются раньше, чем собственные обещания. До версии 3.5 Bluebird обещает использовать setImmediate как средство для их планирования.

Так выглядит макет

Давайте попробуем понять это на примере

Promise.resolve (). then (() = ›console.log (‘ promise1 callback ’));

Promise.resolve (). then (() = ›{

console.log («обещание2 обратного вызова»);

process.nextTick (() = ›console.log (« следующий тик внутри обещания2 »));

});

setImmediate (() = ›console.log (« немедленный1 обратный вызов »));

setImmediate (() = ›console.log (« немедленный2 обратный вызов »));

process.nextTick (() = ›console.log (« обратный вызов следующего тика1 »));

process.nextTick (() = ›console.log (‘ next tick2 callback ’));

setTimeout (() = ›console.log (« установить тайм-аут »), 0);

setImmediate (() = ›console.log (« немедленный3 обратный вызов »));

В приведенном выше примере у нас есть 2 обещания, 3 - немедленно, 3 - nextTick и 1 - обратный вызов таймаута.

Поэтому, когда цикл начинается, он проверяет очереди nextTick и microTasks (собственные обещания). Так он печатает

обратный вызов next tick1

обратный вызов next tick2

обратный вызов обещания1

обратный вызов обещания2

Как только это будет сделано, цикл снова проверяет, есть ли еще несколько nextTicks, и выполняет

Итак, он выполняет и печатает

следующий тик внутри обещания2

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

установить тайм-аут

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

Так он печатает

обратный вызов next tick1

обратный вызов next tick2

В общем, так работают разные фазы цикла событий в случае libuv в сочетании с двумя очередями nodejs.

Этот механизм - то, что делает nodejs невероятно масштабируемым для обработки одновременных рабочих нагрузок ввода-вывода с большим количеством одновременных операций ввода-вывода. Он использует libuv в качестве мультиплексора ввода-вывода, который позволяет обрабатывать тысячи TCP-соединений в одном потоке. Модель потока на соединение не является масштабируемой моделью для рабочих нагрузок с интенсивным вводом-выводом в сети.

Заявление об ограничении ответственности: мнения, выраженные выше, являются личными и не относятся к компании, в которой я работаю.