NodeJS event-loop для самых маленьких

Я у мамы стал совсем большой и дорос до того, что сам провожу технические собеседования на позицию NodeJS разработчика. Одним из важных факторов оценки кандидата я считаю понимание принципов работы NodeJS. Ох, чего мне только там не рассказывали. Возможно, я уже услышал все комбинации слов: очередь, таска, промис и ивент луп. В самом деле, концепция кооперативной многозадачности (aka event-loop) не очень проста, но в этом посте я постараюсь на пальцах объяснить идею того, как работает event-loop.

Код!

Имеется нехитрый обработчик HTTP запросов, который для простоты понимания выполняется за 100 миллисекунд. Уточню, я имею в виду то, что 100 миллисекунд - это время от начала открытия сетевого соединения до момента закрытия этого сетевого соединения. Функция Db.getUserById - это запрос в базу данных, которая асинхронно обрабатывает входящие запросы.

const express = app();

app.get('/users/:id', async (req, res) => {
  const user = await Db.getUserById(req.params.id);

  res.json({ user });
});

express.listen(8080);

Клиентский тоже не хитрый. Функция request принимает идентификатор пользователя, делает HTTP запрос и выводит на экран время, которое занял этот HTTP запрос. Функция test запускает два параллельных запроса, дожидается ответа от обоих запросов и выводит время, которое заняли запросы.

async function request(id) {
  const now = Date.now();

  const res = await fetch(`http://localhost:8080/users/${id}`);

  await res.text();

  const elapsed = Date.now() - now;

  console.log(`Request ${id} elapsed time: ${elapsed}`);
}

async function test() {
  const now = Date.now();

  await Promise.all([
    request(10),
    request(20),
  ]);

  const elapsed = Date.now() - now;

  console.log(`Total elapsed time: ${elapsed}`);
}

test();

Вопросы!

  • За какое время выполнится один запрос?
  • За какое время выполнятся оба запроса?
  • Почему так происходит?

Аналогия и театр

Я бы мог устроить душный разгон про очереди, тики, микро и макро таски, колбеки и все прочее, но я приведу условный пример из жизни, а потом переложу его на те термины и процессы, которые происходят в NodeJS.

Сюжет и действующие лица

Начальник отдела кадров - Сергей Базоданнов, его секретарь - Таня Ивентлупова и два брата Реквестовы Петя и Ваня.

Петя и Ваня Реквестовы хотят устроиться в компанию. Они заполнили свои анкеты и отправились лично отнести их в компанию.

Сцена первая. Подача документов

В компании Петю и Ваню отправили к секретарю - Тане Ивентлуповой. Таня, увидев перед собой двух ярых кандидатов, в свойственной ей манере, попросила встать их в очередь потому, что она одна и не может принять две анкеты параллельно, а может только синхронно по одной. Петя и Ваня, несмотря на то, что пришли одновременно, выстроились в очередь и по очереди отдали свои анкеты. Таня в порядке очереди приняла анкеты, завела себе в ежедневнике задачи обработать анкеты и сказала Пете и Ване, что сделает колбек (она так перезвонить называет), после того как начальник посмотрит анкеты. Радостные Петя и Ваня уходят домой ждать, когда Таня сделаем им колбек.

Сцена вторая. Таня и её метод работы

Сергей Базоданнов - суровый начальник, но всегда готов ответить Тане в любое время. Таня Ивентлупова привыкла работать иначе. Она относит бумажки не сразу после того как она взяла их в работу, а только в определенное время тика. Тиком Таня называет цикл своего рабочего дня, один из этапов которого - это поход в кабинет начальника для того, чтобы оставить ему новые документы и забрать уже подписанные. Вот так вот, во время очередного тика, Таня приняла еще несколько бумажек и пошла в кабинет начальника отдать собранные ею за время тика документы. Каждый документ она подписала номером задачи из своего ежедневника.

Сцена третья. Колбеки

Начальник Сергей посмотрел анкеты Пети и Вани, принял решение, что примет на работу обоих, написал что-то на анкетах и положил их в папку, которую позже заберет Таня. Таня, следуя своему распорядку дня в виде так называемого ею тика, забрала бумажки, пришла на свое рабочее место и стала разбирать взятые ею документы, сверяясь со своим ежедневником. Дошла очередь и до анкет Пети и Вани. Таня, как и обещала, сделала колбек к каждому из кандидатов и пригласила забрать их свои анкеты с резолюциями начальника. Петя и Ваня Реквестовы на радостях прибежали в компанию, в порядке очереди забрали свои анкеты, после чего радостные пошли смотреть, что им написал Сергей на анкетах.

Анализ спектакля

Начальник Сергей Базоданнов - это наша база данных, которая асинхронно обрабатывает входящие запросы. База данных может принять запрос в любой момент времени вне зависимости от того, на каком этапе event-loop сейчас.

Петя и Ваня Реквестовы - это асинхронные HTTP запросы. Каждый из запросов сам по себе является по настоящему асинхронным и независимым так же как и Петя с Ваней.

Таня Ивентлупова - это NodeJS, в основе которого лежит архитектура event-loop. Сам по себе NodeJS работает синхронно, поэтому все входящие асинхронные запросы он обрабатывает синхронно, в том порядке, в котором они пришли. Именно из-за этого Таня просила выстроиться в очередь Петю и Ваню т.к. параллельно она не может обрабатывать их анкеты. Более того, чтобы эффективно использовать свое рабочее время Таня, как и event-loop имеет четкий распорядок дня в виде последовательности действий, которые называются тиком. Но самое главное и тут то, что Таня обрабатывает свои задачи порциями, в том числе и взаимодействие со своим начальником. Если бы Таня носила каждую бумажку сразу после того как приняла её в работу, то вся очередь была бы заблокирована ожиданием того когда Таня вернется, чтобы принять в работу следующую бумажку.

Код! Снова код!

Теперь посмотрим на код через призму наших персонажей и сцен. В семпле ниже персонаж Таня, олицетворяющая event-loop, сокрыта от нас и действует неявно, но я постараюсь подсветить все моменты.

const express = app();

app.get(
  '/users/:id',
  // Тут Таня приняла анкету на обработку
  // В данном случае обработка анкеты это три этапа
  // 1й - Таня вызвала человека из очереди. По большей части от сокрыт от нас
  // 2й - Таня получила анкету, записала задачу в свой ежедневник и пообещала перезвонить. Все эти действия происходят до await
  // 3й - Таня получила ответы от начальника и делает звоник (aka callbacks) всем, для кого ответ готов. Все действия после await
  async (req, res) => {
    // тут Таня записывает задачу в свой ежедневник о том, что она работает с какой-то анкетой
    // тут же Таня дает обещание (aka Promise) о том, что она оповестит об решении, когда оно поступит
    const form = await Database.reviewAndSignForm(req.params.id);

    // тут Таня уже всех оповестила (сделала колбеки) и отдает анкеты (вызов res.json)

    res.json({ form });
  }
);

express.listen(8080);

Ответы и выводы

Если вы осилили мой графоманский спектакль и его анализ, то теперь можно получить ответы на поставленные вопросы.

За какое время выполнится один запрос?

За 100 миллисекунд. В реальности есть всякие разные издержки: сеть может работать не стабильно, задач в очереди может быть много и множество других факторов, но в наших лабораторных условиях будет 100 миллисекунд.

За какое время выполнятся оба запроса?

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

Почему так происходит?

NodeJS, в основе которого лежит event-loop, не блокирует поток операционной системы во время обработки асинхронных задач. Вместо этого он запускает асинхронную обработку, регистрирует её и продолжает обрабатывать задачи, которые он взял в текущий тик. В свою очередь, когда асинхронная задача завершается она, вызывает колбек, который ей выдал NodeJS и в ближайшем тике NodeJS возьмет задачу в обработку.

Опубликовано: 17/11/2023
Ключевые слова: NodeJS, event-loop

Автор: Арутр Аралин
Telegram: @aaralin, email: username@aaralin.ru
Лайк, подписка, колокольчик