Как работает цикл событий в Node.js
23 ноября 2022 г.Node.js — это однопоточная платформа, управляемая событиями, способная выполнять неблокирующее асинхронное программирование. Эти функции Node.js позволяют эффективно использовать память. Event Loop позволяет Node.js выполнять неблокирующие операции ввода-вывода, несмотря на то, что JavaScript является однопоточным. Это делается путем назначения операций операционной системе всегда и везде, где это возможно.
В этой статье я попытаюсь объяснить механизм параллелизма в Node.js, который называется циклом событий.
Первое, что нужно учитывать, это то, что Node.js написан только на Javascript, а для правильной работы — на C++ и Javascript. Существует множество библиотек, от которых зависит node, но две главные зависимости, которые обрабатывают большинство операций node.js, V8 и LIBUV.
Эти две зависимости позволяют нам писать чистый код JavaScript, который работает в Node.js, и в то же время дают нам доступ к функциям чтения файлов, реализованным в LIBUV.
В этой библиотеке также реализован механизм цикла обработки событий.
Давайте откроем исходный код этой библиотеки и посмотрим, как реализован механизм. Исходный код LIBUV написан на C для unix, показанный ниже (но вы также можете найти win):
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int can_sleep;
r = uv__loop_alive(loop); // Check loop alive ----- (1)
if (!r)
uv__update_time(loop);
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop);
uv__run_timers(loop); // Check timer phase ----- (2)
can_sleep =
QUEUE_EMPTY(&loop->pending_queue) && QUEUE_EMPTY(&loop->idle_handles);
uv__run_pending(loop); // Check pending callbacks ----- (3)
uv__run_idle(loop); // Check idle ------ (4)
uv__run_prepare(loop); // Check prepare ----- (5)
timeout = 0;
if ((mode == UV_RUN_ONCE && can_sleep) || mode == UV_RUN_DEFAULT)
timeout = uv__backend_timeout(loop);
uv__metrics_inc_loop_count(loop);
uv__io_poll(loop, timeout); // Check poll ----- (6)
for (r = 0; r < 8 && !QUEUE_EMPTY(&loop->pending_queue); r++)
uv__run_pending(loop);
uv__metrics_update_idle_time(loop);
uv__run_check(loop); // Check run checks ------ (7)
uv__run_closing_handles(loop); // Check close callbacks ------ (8)
if (mode == UV_RUN_ONCE) {
uv__update_time(loop);
uv__run_timers(loop);
}
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
return r;
}
Составим схему работы на схеме. На следующей диаграмме показан упрощенный обзор порядка операций цикла обработки событий.
Состоит из двух частей Микрозадачи и Макрозадачи, которые разделены на фазы (каждая ячейка будет называться «фазой» цикла событий). .
Обзор этапов
- Таймеры: на этом этапе выполняются обратные вызовы, запланированные с помощью
setTimeout()
иsetInterval()
. - Ожидаемые обратные вызовы: выполняются обратные вызовы ввода-вывода, отложенные до следующей итерации цикла.
- Idle, Prepare: используется только для внутреннего использования.
- Опрос: получение новых событий ввода-вывода; выполнять обратные вызовы, связанные с вводом-выводом (почти все, за исключением обратных вызовов закрытия, тех, которые запланированы таймерами, и
setImmediate()
); node будет блокироваться здесь, когда это уместно. - Проверьте: здесь вызываются обратные вызовы
setImmediate()
. - Обратные вызовы закрытия: некоторые обратные вызовы закрытия, например.
socket.on('close', ...)
.
Каждая фаза имеет очередь FIFO для выполнения обратных вызовов. Когда цикл событий входит в заданную фазу, он будет выполнять любые операции, характерные для этой фазы, а затем выполнять обратные вызовы в очереди этой фазы, пока либо очередь не будет исчерпана, либо пока не будет выполнено максимальное количество обратных вызовов. Когда очередь исчерпана или достигнут лимит обратных вызовов, цикл обработки событий перейдет к следующему этапу.
Также эта библиотека отвечает за предоставление Nodejs многопоточности или возможность предоставления пула потоков в процессе Nodejs для выполнения синхронных задач. Пул потоков состоит из четырех потоков (можно настроить до 128), созданных для выполнения ресурсоемких задач, которые не должны выполняться в основном потоке. И с такой настройкой наше приложение не блокируется этими задачами.
некоторые API — —, перечисленные ниже, используют пул потоков, созданный libuv:
- dns.lookup()
- Все синхронные API zlib
- Все синхронные API fs, кроме fs.FSWatcher()
- API асинхронного шифрования
Приведенный выше список можно разделить на операции с интенсивным использованием ЦП и операции с интенсивным вводом-выводом.
Давайте посмотрим на следующий код и на то, как он будет обрабатываться
const fs = require('fs');
const http = require('http');
const requestListener = function (req, res) {
res.writeHead(200);
res.end('Hello, World!');
}
const server = http.createServer(requestListener);
server.listen(3000);
console.log("Start"); // ----- (A)
setTimeout(() => console.log("setTimeout -- 1"), 0); // ----- (B)
setImmediate(() => console.log("setImmediate")); // ----- (C)
fs.readFile(__filename, () => { // ----- (D)
console.log("readFile callback"); // ----- (E)
setTimeout(
() => console.log("setTimeout in readFile"),
0
); // ----- (F)
setImmediate(
() => console.log("setImmediate in readFile")
); // ----- (G)
process.nextTick(
() => console.log("nextTick in readFile")
); // ----- (H)
});
Promise.resolve().then(() => { // ----- (I)
console.log("Promise success callback"); // ----- (J)
process.nextTick(
() => console.log("Promise nextTick")
); // ----- (K)
setImmediate(
() => console.log("Promise setImmediate")
); // ----- (L)
});
process.nextTick(() => console.log("nextTick")); // ----- (M)
setTimeout(() => console.log("setTimeout -- 2"), 0); // ----- (N)
console.log('End'); // ----- (O)
Поэтому, когда Node.js читает файл, он синхронно выполняет консоли (A, O). Остальные асинхронные операции будут разделены на микро- и макрозадачи, после выполнения синхронного кода наш цикл событий начинает работать. р>
Порядок выполнения асинхронных операций представлен ниже:
Заключение
Первое, что нужно отметить, это то, что ваш код в Nodejs является однопоточным. И это не значит, что Node работает в одном потоке. Вопрос «Является ли Node однопоточным?» всегда сбивает с толку, потому что Node работает на V8 и Libuv.
Сам по себе V8 работает одним потоком, но с помощью библиотеки Libuv используется цикл событий, распределяет операции по разным очередям (фазам), а для некоторых тяжелых операций (сначала отправляется в пул потоков для исполнения разными потоками, затем поднятые возвращаются в фазу), после чего Event Loop, в соответствии с лежащим в основе механизмом, начинает отправлять обратные вызовы для выполнения в основной поток.
Это и есть весь механизм параллелизма в Node.js, который называется циклом событий.
Надеюсь, это было полезно для вас!
Спасибо за чтение!
Оригинал