NodeJS | Воркеры. Рабочие потоки (Worker threads). Многопоточность.

NodeJS | Воркеры. Рабочие потоки (Worker threads). Многопоточность.

Когда нужны «Воркеры» (Worker threads) в NodeJS? Почему тормозит NodeJS?

Лучше спрашивать не «Зачем нужны«, а «Когда нужны» Воркеры. NodeJS — это однопоточная среда. То есть на один процесс приходится один поток. И в какой-то внезапный момент времени этот поток может забиться и перестать быстро работать.

Вот ты разработчик. Пишешь, пишешь себе код потихоньку и тут на тебе, производительность падает. Вроде и процессор мощный. Вроде и памяти предостаточно. Но после написания дополнительного функционала в приложении, всё люто начинает тормозить. Что меняется? Может ты чего-то не знаешь?

 

Чем забивается поток в NodeJS?

Самую большую нагрузку на поток несут «долгие синхронные вычисления«. Это когда ты пытаешься что-то сложно вычислять — хэш-функции, криптография, сложные регулярные выражения по огромному массиву строк, циклы в циклах и ещё раз в циклах, лютая высшая математика и тому подобное.

Так вот вся проблема начинается тогда, когда «цикл событий» не может пробиться в это «долгое синхронное вычисление«. И что происходит тогда? Очень быстро ничинают переполняться очереди «макро/микро задач». Начинает сильно возрастать «задержка шага цикла событий«.

Этот параметр называется «Event Loop Latency p95» и «Event Loop Latency«. Если ты работаешь с менеджером процессов PM2, то значение в более 100 миллисекунд будет говорить о проблеме в разработке. Чем больше это число, тем «тупее» будет работать NodeJS. Важно отметить, что работать будет (не отвалится), но медленно.

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

 

Простое объяснение многопоточности в NodeJS

Если по-простому рассказывать про поток, то представьте себе дорогу по которой едут автомобили. Если вы живёте в крупном городе, то я рекомендую обратить внимание на КАД (Кольцевую Автомобильную Дорогу). Это такая дорога, по которой в любое время дня и ночи проезжают автомобили.

Давайте будем говорить только об одном направлении движения. Теперь я задам вам вопрос — Какова скорость потока автомобилей по этой дороге? 140? 90? 50? Не парьтесь. Правильного ответа тут нет, если мы не учитываем время, в которое делаем расчёт скорости потока автомобилей.

Согласитесь, что в будние дни в 12 часов дня количество автомобилей на дороге будет больше, чем в 12 часов ночи этого же дня.

А теперь представьте себе ситуацию: лето, пятница, вечер, хорошая солнечная погода, шашлыки, банька! Все валят из города по своим пригородным дачам и садоводствам. Если вы на авто, то выезд из города в это время создаст для вас проблему. Вы встанете в пробку в 98% подобных случаев. Ваша скорость потока будет 1 километр в час.

Теперь посмотрим на это со стороны. Вроде у вас мощный автомобиль, который легко разгонится до 120. Вроде есть дорога, которая НЕ ВСЕГДА в пробке. Но вы не едете. И причём не только вы. Все остальные также «тормозят». Что происходит? Может проблема в количестве участников дорожного движения?

Думаю вы догадались, что вся проблема в дороге, которая просто не справляется с таким потоком автомобилей. Какое решение проблемы вам кажется самым верным в данной ситуации? Оно первым придёт вам на ум.

Решение: ВАМ НУЖНА ЕЩЁ ОДНА ДОРОГА !!!

Часть автомобилей поедут по одной дороге, а часть поедет по другой. И тогда все будут «ехать», а не «стоять».

Надеюсь, аналогия понятна. Теперь перейдём обратно на уровень языка программирования JavaScript (NodeJS).

  1. Поток — это дорога с одной полосой в одном направлении. (у процессора есть только вход и выход, и каждый такт переносит электроны от входа к выходу)
  2. Сложная, долгая синхронная функция — это медленный гружёный автомобиль.
  3. Задержка цикла событий — это время, за которое этот участок дороги проезжает наш медленный автомобиль.

 

Зачем нужны «Воркеры» (Worker threads) в NodeJS?

Теперь мы можем дать ответ. Воркеры (Worker threads) в NodeJS нужны для того, чтобы вынести в отдельный поток долгое синхронное вычисление какой-то сложной функции.

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

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

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

 

Почему поток в NodeJS ограничен в ресурсах?

Это просто чей-то расчёт. Просто разработчики NodeJS договорились, что один поток должен «откусывать» от доступного ему процессора маленький кусочек тактовой частоты. Почему так? Я не знаю. Нужно икать информацию.

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

 

Многопоточность и Асинхронность в NodeJS

Никогда не связывайте эти два понятия в одно. Это разные вещи!!!

Асинхронность — это когда вы хотите на некоторое время съехать с дороги на обочину и постоять там неопределённое (или определённое) время. От того что вы съехали на обочину, дорога не поменялась. Вы всё равно захотите однажды вернуться на эту же дорогу. В момент вашего возврата обратно в поток, вы ещё сильнее замедлите его работу так как вам должны будут уступить место на дороге. Так вот если таких «обочечников» много, то дорожная ситуация от съезда/заезда на обочину не изменится. Если на дороге «пробка», то обочина тоже в «пробке».

Многопоточность — это несколько отдельных дорог. Чем больше дорог, темы выше вероятность, что всем автомобилям их будет хватать. Если всем авто хватает дорог, то не имеет значения на какой дороге вы съехали/заехали с обочины.

 

Долгая синхронная функция в NodeJS

Теперь рассмотрим пример долгой синхронной функции на JavaScript (NodeJS). По сути нам нужен цикл, который выполняет большое количество итераций с какой-то сложной математической операцией — например, получение квадратного корня числа.

Долгая синхронная функция на JavaScript
Долгая синхронная функция на JavaScript
function f_sync_long(obj = {}){
   // Фиксируем время начала выполнения функции
   obj.start_time = Date.now(); // Прописываем ключ для объекта
   console.log(`Старт в: ${obj.start_time}`);

   // Получаем случайное количество итераций для цикла
   // Имитируем разный объём данных для синхронной обработки
   obj.iterations = Math.ceil(10**9 * Math.random());

   // Накапливаем вычисления в переменную
   obj.total = 0;

   // Запускаем цикл
   for(let x = 0; x < obj.iterations; x++){
      // Условная дополнительная нагрузка на функцию через вычисление корней
      // для каждой пятой итерации
      if(!(x%5)){
         obj.total += Math.sqrt(x*10);
      };
      // Постоянная нагрузка для каждой итерации цикла
      obj.total += Math.sqrt(x*10);
   };

   obj.delay_time = Date.now() - obj.start_time;
   obj.end_time = Date.now();

   console.log(`Финиш в: ${obj.end_time}`);
   console.log(`Затрачено времени: ${obj.delay_time}`);

   // Возвращаем созданный объект
   return obj;
};

Каждый вызов данной функции в консоли браузера будет вызывать разные задержки выполнения.

Разное время задержки выполнения долгой синхронной функции в JavaScript
Разное время задержки выполнения долгой синхронной функции в JavaScript

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

На скриншоте можно увидеть задержки в 2,8 секунд, 4,5 секунды и 1 секунда. Это очень долго.

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

Такое долгое вычисление нужно выносить в отдельный поток и высчитывать отдельной частотой процессора.

 

Информационные ссылки

Стандарт NodeJShttps://nodejs.org/api/worker_threads.html

Цикл событий JavaScripthttps://html.spec.whatwg.org/multipage/webappapis.html#event-loops

Цикл событий NodeJShttps://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick

Дэвид Хеттлер — https://davidhettler.net/blog/event-loop-lag/