Рабочие потоки в Node.js (обзор)

Рабочий поток был представлен в Node v10.x как экспериментальный API и теперь стабилен с версии v12.x. Как мы все знаем, однопоточная модель Nodejs (хотя у нее есть пул потоков Libuv) хорошо подходит для неблокирующей операции ввода-вывода. Но Node.js показывает низкую производительность при интенсивном использовании ЦП (блокирующий код), потому что Node выполняет блокирующий код в основном потоке и блокирует другой код для выполнения. Основная цель этого поста - охватить:

  • Обзор рабочих потоков.
  • Чем они отличаются от обычных потоков?
  • Зачем и когда использовать рабочие потоки?

Задачи с интенсивным использованием ЦП

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

Золотое правило Node.js
Не блокируйте цикл обработки событий, продолжайте его работу и избегайте всего, что может заблокировать потокоподобные синхронные сетевые вызовы или бесконечные циклы.

JavaScript и Node.js не предназначены для использования для задач, связанных с процессором. Поскольку JavaScript является однопоточным, это заморозит пользовательский интерфейс в браузере и поставит в очередь любое событие ввода-вывода в Node.js.

Важно различать операции ЦП и операции ввода-вывода (ввода-вывода). Код Node.js НЕ выполняется параллельно. Параллельно выполняются только операции ввода-вывода, поскольку они выполняются асинхронно.

Существующие решения

У Nodej уже есть API, такие как «cluster» и «child-process», и они стабильны, и их можно использовать для достижения параллелизма и выполнения задач, интенсивно использующих ЦП. Но не может быть идеальным решением из-за:

  1. API кластера: модуль кластера обеспечивает параллелизм, порождая рабочий (дочерний) процесс в ядрах ЦП. Рабочие процессы создаются с использованием метода child_process.fork(), чтобы они могли взаимодействовать с родителем через IPC и передавать серверные дескрипторы туда и обратно. Но что делать, если у вас только одноядерный процессор.
  2. API дочернего процесса: метод child_process.spawn(),child_process.fork() создает дочерний процесс асинхронно, не блокируя цикл событий Node.js. Функция child_process.spawnSync() обеспечивает эквивалентную функциональность синхронным способом, блокируя цикл обработки событий до тех пор, пока порожденный процесс не завершится или не завершится.
  3. Но создание процесса не всегда является хорошим решением, если оно не требуется, потому что создание рабочего (дочернего) процесса потребляет много системных ресурсов.

Помните:

1. Создание дочернего процесса - операция не из дешевых, она требует больших затрат ресурсов ОС.

2. Потоки по-прежнему очень легкие с точки зрения ресурсов по сравнению с разветвленными процессами. И это причина, по которой родились рабочие потоки!

Итак, каково решение для Nodejs?

Ответ: рабочие потоки

Рабочие потоки в некотором роде аналогичны WebWorkers в браузере. В браузерах давно существует концепция рабочих. Рабочие потоки позволяют использовать потоки, которые выполняют JavaScript параллельно.

Один из способов сделать код блокировки неблокирующим - выполнить его в отдельном потоке. Таким образом, основной поток может продолжить работу над другими вещами, поскольку операция блокировки блокирует совершенно другой поток. Это решение, в котором Node.js использует рабочий поток.

Чтобы понять Workers, во-первых, необходимо понять, как устроен Node.js.

Когда процесс Node.js запускается, он запускается:

  • Один процесс
  • Один поток
  • Один цикл событий
  • Один экземпляр JS Engine
  • Один экземпляр Node.js

Один процесс: процесс - это глобальный объект, к которому можно получить доступ из любого места и который содержит информацию о том, что выполняется одновременно.

Один поток: однопоточный означает, что в данном процессе одновременно выполняется только один набор инструкций.

Один цикл событий: это один из наиболее важных аспектов, которые нужно понять о Node. Это то, что позволяет Node быть асинхронным и иметь неблокирующий ввод-вывод, несмотря на то, что JavaScript является однопоточным, за счет передачи операций ядру системы, когда это возможно, через обратные вызовы, обещания и async / await.

Один экземпляр JS Engine: это компьютерная программа, которая выполняет код JavaScript.

Один экземпляр Node.js: компьютерная программа, выполняющая код Node.js.

Другими словами, Node работает в одном потоке, и в цикле событий одновременно выполняется только один процесс. Один код, одно выполнение (код не выполняется параллельно). Это очень полезно, поскольку упрощает использование JavaScript, не беспокоясь о проблемах параллелизма.

С другой стороны, рабочие потоки имеют:

  • Один процесс
  • Несколько потоков
  • Один цикл событий на поток
  • Один экземпляр JS Engine на поток
  • Один экземпляр Node.js на поток

Как мы видим на следующем изображении:

Рабочие потоки имеют изолированные контексты. Они обмениваются информацией с основным процессом, используя передачу сообщений, поэтому мы избегаем проблем с условиями гонки, которые возникают у потоков! Но они живут в одном процессе, поэтому используют намного меньше памяти.

Модуль worker_threads позволяет использовать потоки, которые выполняют JavaScript параллельно. Чтобы получить к нему доступ:

const worker = require('worker_threads');

Идеально иметь несколько экземпляров Node.js внутри одного процесса. С рабочими потоками поток может завершиться в какой-то момент, и это не обязательно конец родительского процесса. Не рекомендуется, чтобы ресурсы, выделенные Worker'ом, оставались без дела, когда Worker ушел - это утечка памяти, а мы этого не хотим. Мы хотим встроить Node.js в себя, дать Node.js возможность создать новый поток, а затем создать новый экземпляр Node.js внутри этого потока; по сути, запуск независимых потоков внутри одного и того же процесса.

Лучшее решение для интенсивных вычислений с использованием ЦП в Node.js - запуск нескольких экземпляров Node.js внутри одного процесса, при этом память может использоваться совместно и нет необходимости передавать данные через JSON. Именно это и делают рабочие потоки в Node.js.

child_process.fork()1_isolate.html">Изолировать представляет собой изолированный экземпляр движка child_process.fork()1_v8.html">V8 и играет важную роль для создания рабочих потоков. Isolates, как следует из названия, полностью закрыты для внешнего мира, поэтому Isolates могут работать параллельно, поскольку они полностью представляют собой разные экземпляры V8.

Что делает Worker Threads особенными:

  • ArrayBuffers для передачи памяти из одного потока в другой
  • SharedArrayBuffer, который будет доступен из любого потока. Он позволяет вам разделять память между потоками (ограничиваясь двоичными данными).
  • Atomics доступен, он позволяет выполнять некоторые процессы одновременно, более эффективно и позволяет реализовать переменные условий в JavaScript.
  • MessagePort, используется для связи между разными потоками. Его можно использовать для передачи структурированных данных, областей памяти и других портов сообщений между разными рабочими процессами.
  • MessageChannel представляет собой асинхронный двусторонний канал связи, используемый для связи между различными потоками.
  • WorkerData используется для передачи данных запуска. Произвольное значение JavaScript, которое содержит клон данных, переданных конструктору Worker этого потока. Данные клонируются, как при использовании postMessage()

API

  • const { worker, parentPort } = require(‘worker_threads’) = ›Класс worker представляет независимый поток выполнения JavaScript, а parentPort - экземпляр порта сообщения
  • new Worker(filename) или new Worker(code, { eval: true }) = ›- два основных способа запуска воркера (передача имени файла или кода, который вы хотите выполнить). В производственной среде рекомендуется использовать имя файла.
  • worker.on(‘message’), worker/postMessage(data) = ›для прослушивания сообщений и их отправки между разными потоками.
  • parentPort.on(‘message’), parentPort.postMessage(data) = ›Сообщения, отправленные с использованием parentPort.postMessage(), будут доступны в родительском потоке с использованием worker.on('message'), а сообщения, отправленные из родительского потока с использованием worker.postMessage(), будут доступны в этом потоке с использованием parentPort.on('message').

Пример:

Примеры: https://github.com/sandeepp2016/Nodejs-worker-threads-examples

Важные моменты:

  • имейте в виду, что создание Worker (например, потоков на любом языке), даже если это намного дешевле, чем разветвление процесса, также может использовать слишком много ресурсов в зависимости от ваших потребностей. В этом случае в документации рекомендуется создать пул рабочих.
  • Не используйте Workers для распараллеливания операций ввода-вывода.
  • Не думайте, что создание рабочих - это дешево.
  • Есть еще несколько типов состояний гонки, которые могут возникнуть, и это связано с доступом к внешним ресурсам. Например, если два рабочих потока или процесса пытаются заниматься своими делами и записывать в один и тот же файл, они могут явно конфликтовать друг с другом и создавать проблемы.

Заключение

  1. Основная цель Workers - повысить производительность операций с интенсивным использованием ЦП, а не операций ввода-вывода. Поэтому используйте их только в том случае, если вам нужно выполнять ресурсоемкие задачи с большими объемами данных.
  2. Таким образом, рабочие потоки не очень помогут при работе с интенсивным вводом-выводом, потому что асинхронные операции ввода-вывода более эффективны, чем рабочие.
  3. Вы можете совместно использовать память с рабочими потоками. Вы можете передавать объекты SharedArrayBuffer, специально предназначенные для этого.

Ссылки:

В конце концов

Надеюсь, вам понравилось это читать. Не стесняйтесь оставлять свои отзывы. Пожалуйста, поделитесь им с друзьями, если он вам понравился.

Примечание: Часть 2 уже опубликована