Цикла сообщений Nodejs действительно достаточно, чтобы просмотреть одну статью

Node.js задняя часть

очередь сообщений Nodejs

Цикл сообщений в Nodejs и браузерах может быть разным, здесь мы обсуждаем только принцип Nodejs Event Loop.

Исходный код относится к последней версии Nodejs v9.7.1, соответствующей версии V8 v6.2.414, а libUV — v1.19.2.

Общая архитектура Nodejs

Говоря об архитектуре Nodejs, давайте сначала перейдем к взаимосвязи и роли Nodejs с V8 и libUV:

  • V8: Движок, который выполняет JS, то есть переводит JS, включая оптимизацию компиляции, сборку мусора и т. д., с которыми мы знакомы.
  • libUV: обеспечивает асинхронный ввод-вывод, обеспечивает цикл обработки сообщений. Видимый, это уровень абстракции уровня API операционной системы.

Так как же Nodejs их организует, как показано ниже:

Nodejs передает JS в V8 через слой привязки C++.После того, как V8 анализирует его, он передает его libUV для инициации ввода-вывода asnyc и ожидает планирования цикла обработки сообщений.Посмотрите на следующий рисунок:

Модель потоков Nodejs

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

Но это не означает, что процесс Node имеет только один поток.Node.js event loop workflow & lifecycle in low levelсказал в:

the fact is thread-pool is something in libUV library (used by node for third party asynchronous handling) ... However, ... like, file reading, making request to a different host, dns lookup etc., are handled by the thread-pool, which uses only 4 threads by default...

Соответствующий исходный код находится здесь:libuv/src/threadpool.c#38

На уровне libUV на самом деле существует пул потоков, помогающий выполнять некоторую работу.

Проработка цикла сообщений

Давайте снова посмотрим на часть цикла сообщений в JS:

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

Timer Phase

Это первая стадия цикла обработки сообщений, использующая цикл for для обработки всехsetTimeoutа такжеsetIntervalобратный вызов. Код здесь:src/unix/timer.c#150

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

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

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

Pending I/O Callback Phase

Этот этап заключается в выполнении вашегоfs.read, сокет и другие функции обратного вызова операций ввода-вывода, а также различные обратные вызовы ошибок.Исходный код находится здесь:src/unix/core.c#763

Idle, Prepare Phase

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

Poll Phase

Это самая важная фаза во всем цикле обработки сообщений, которая используется для ожидания асинхронных запросов и данных (оригинал:accepts new incoming connections (new socket establishment etc) and data (file read etc)).

Говорят, что он самый важный, потому что он поддерживает весь механизм цикла сообщений.Фаза опроса будет выполняться первой.watch_queueЗапросы ввода-вывода в очереди, один разwatch_queueЕсли очередь пуста, весь цикл сообщений переходит в спящий режим (на разных платформах используются разные технологии. Например, epoll используется в Linux, IOCP в Win и kqueue в OS X), таким образом, ожидая пробуждения ядром. события. Исходный код здесь:src/unix/linux-core.c#188

Конечно, фаза опроса не может ждать вечно. Она хорошо продумана. Проще говоря,

1. 它首先会判断后面的 Check Phase 以及 Close Phase 是否还有等待处理的回调. 如果有, 则不等待, 直接进入下一个 Phase. 
2. 如果没有其他回调等待执行, 它会给 epoll 这样的方法设置一个 timeout. 可以猜一下, 这个 timeout 设置为多少合适呢? 答案就是 Timer Phase 中最近要执行的回调启动时间到现在的差值, 假设这个差值是 detal. 因为 Poll Phase 后面没有等待执行的回调了. 所以这里最多等待 delta 时长, 如果期间有事件唤醒了消息循环, 那么就继续下一个 Phase 的工作; 如果期间什么都没发生, 那么到了 timeout 后, 消息循环依然要进入后面的 Phase, 让下一个迭代的 Timer Phase 也能够得到执行.

Nodejs управляет всем циклом обработки сообщений через фазу опроса, ожидая событий ввода-вывода и поступления асинхронных событий ядра.

Check Phase

Далее следует этап проверки. Этот этап касается толькоsetImmediateфункция обратного вызова.

Так почему же здесь особое отношение?setImmediateА как насчет фазы?Проще говоря, потому что фаза опроса может установить некоторые обратные вызовы, надеясь запуститься после фазы опроса.Поэтому эта фаза проверки добавляется после фазы опроса.

Close Callbacks Phase

Специально обрабатывать некоторые обратные вызовы закрытого типа, напримерsocket.on('close', ...), Используется для очистки ресурсов.

process.nextTick и обещания

Видно, что схема цикла сообщений не включаетprocess.nextTickтак же какPromiseТак в чем заключается особая линейность этих двух обратных вызовов?

Эта очередь сохраняется первой, чтобы убедиться, что всеprocess.nextTickобратный звонок, то всеPromiseОбратный вызов добавляется сзади и, наконец, извлекается и выполняется одновременно в конце каждой фазы.

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

Давайте посмотрим на картинкуprocess.nextTickтак же какPromise:

FAQ

setTimeout(..., 0) vs. setImmediateКто быстрее?

Давайте возьмем пример, чтобы почувствовать это.

Это классический вопрос интервью FE:

Что выводит следующий код:

// index.js

setImmediate(() => console.log(2))
setTimeout(() => console.log(1))

Ответ: может быть 1 2, может быть 2 1

Давайте посмотрим на основную проблему этого цикла сообщений с точки зрения принципа.

Во-первых, Nodejs запускается, инициализирует среду и загружает наш JS-код (index.js).Происходят две вещи (на данный момент еще не в цикле сообщений):

`setImmediate` 向 Check Phase 中添加了回调 `console.log(2)`
`setTimeout` 向 Timer Phase 中添加了回调 `console.log(1)`

На данный момент, чтобы завершить фазу инициализации, необходимо войти в цикл обработки сообщений Nodejs, как показано ниже:

Почему выходов два? Следующий шаг имеет решающее значение:

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

  1. Если заданное время обратного вызова в фазе таймера > времени, сохраненного циклом сообщений, будет выполнен обратный вызов в фазе таймера.В этом случае сначала выведите 1, а затем выведите 2, пока не будет выполнена фаза проверки.В общем , результат 1 2.

  2. Если операция выполняется относительно быстро, заданное время обратного вызова в фазе таймера может быть точно равно времени, сэкономленному циклом сообщений.В этом случае обратный вызов в фазе таймера не может быть выполнен, и следующая фаза будет продолжаться. Check Phase, output 2. Затем дождитесь фазы таймера следующей итерации, и время в это время должно соответствовать «предустановленному времени обратного вызова в фазе таймера> времени, сохраненному циклом сообщений», поэтомуconsole.log(1)выполниться, вывести 1. В общем, Результат 2 1.

Следовательно, причина нестабильного вывода зависит от того, соответствует ли время входа в фазу таймера и выполнениеsetTimeoutВремя находится в пределах 1 мс. Если код изменен на следующее, он определенно получит стабильный выход:

require('fs').readFile('my-file-path.txt', () => {
 setImmediate(() => console.log(2))
 setTimeout(() => console.log(1))
});

Выход: 2 1

Это связано с тем, что цикл обработки сообщений вставляет обратные вызовы в очереди Timer и Check на этапе Pneding I/O. В настоящее время, в соответствии с порядком выполнения цикла сообщений, Check должен быть выполнен до Timer, как показано на следующем рисунке:

setTimeout(..., 0)Можно ли заменитьsetImmediateШерстяная ткань?

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

Поговорим о времени выполнения.setTimeout(..., 0)а такжеsetImmediateПолностью относятся к двум фазам. Пожалуйста, обратитесь к 'setTimeout(..., 0) vs. setImmediateКто быстрее?» обсуждение.

Refs

Сведения о таймере узла Node.js event loop workflow & lifecycle in low level The Node.js Event Loop, Timers, and process.nextTick() разные циклы событий исходный код либув