Как работает цикл событий в Node.js

Как работает цикл событий в 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;
}

Составим схему работы на схеме. На следующей диаграмме показан упрощенный обзор порядка операций цикла обработки событий.

Simplified scheme of how EventLoop works

Состоит из двух частей Микрозадачи и Макрозадачи, которые разделены на фазы (каждая ячейка будет называться «фазой» цикла событий). .

Обзор этапов

  • Таймеры: на этом этапе выполняются обратные вызовы, запланированные с помощью 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). Остальные асинхронные операции будут разделены на микро- и макрозадачи, после выполнения синхронного кода наш цикл событий начинает работать.

Tasks in event loop after finish synchronous code

Порядок выполнения асинхронных операций представлен ниже:

Event loop asynchronous operations

Заключение

Первое, что нужно отметить, это то, что ваш код в Nodejs является однопоточным. И это не значит, что Node работает в одном потоке. Вопрос «Является ли Node однопоточным?» всегда сбивает с толку, потому что Node работает на V8 и Libuv.

Сам по себе V8 работает одним потоком, но с помощью библиотеки Libuv используется цикл событий, распределяет операции по разным очередям (фазам), а для некоторых тяжелых операций (сначала отправляется в пул потоков для исполнения разными потоками, затем поднятые возвращаются в фазу), после чего Event Loop, в соответствии с лежащим в основе механизмом, начинает отправлять обратные вызовы для выполнения в основной поток.

Это и есть весь механизм параллелизма в Node.js, который называется циклом событий.

Надеюсь, это было полезно для вас!

Спасибо за чтение!


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE