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 возьмет задачу в обработку.