Полное руководство по многопоточности Node.js

Node.js

Перевод: сумасшедший технический ботаник

оригинал:blog.log Rocket.com/ah-complete-…

Многие люди хотят знать об однопоточныхNode.jsКак он может конкурировать с многопоточным бэкендом. Учитывая его предположительно однопоточный характер, многим крупным компаниям может показаться нелогичным выбирать Node в качестве серверной части. Чтобы понять почему, нужно понять, что на самом деле означает быть однопоточным.

JavaScript предназначен для выполнения относительно простых задач в Интернете, таких как проверка форм или создание разноцветных следов мыши.В 2009 году Райан Даль, основатель Node.jsПозволяет разработчикам писать внутренний код на языке.

Бэкэнд-языки, обычно поддерживающие многопоточность, имеют различные механизмы синхронизации данных между потоками и другие функции, ориентированные на потоки. Добавление поддержки такой функции в JavaScript потребует изменения всего языка, что не является целью Даля. Чтобы чистый JavaScript поддерживал многопоточность, ему пришлось придумать обходной путь. Далее давайте исследовать тайны...

Как работает Node.js

Node.js использует два типа потоков:event loopосновной поток обработки иworker poolНесколько вспомогательных потоков в .

Цикл событий — это механизм, который принимает обратные вызовы (функции) и регистрирует их, готовые к выполнению в какой-то момент в будущем. Он выполняется в том же потоке, что и связанный с ним код JavaScript. Когда операция JavaScript блокирует поток, цикл событий также блокируется.

Рабочий пул — это модель выполнения, которая порождает и обрабатывает отдельные потоки, затем синхронно выполняет задачи и возвращает результаты в цикл обработки событий. Цикл событий выполняет предоставленный обратный вызов с возвращаемым результатом.

Короче говоря, он отвечает за асинхронные операции ввода-вывода — прежде всего за взаимодействие с системным диском и сетью. В основном состоит изfs(интенсивный ввод-вывод) илиcrypto(интенсивная нагрузка на ЦП) и т. д. Для рабочего бассейнаlibuvРеализация, когда Node необходимо внутренне взаимодействовать между JavaScript и C++, вызывает небольшую задержку, но едва заметную.

На основе этих двух механизмов мы можем написать следующий код:

fs.readFile(path.join(__dirname, './package.json'), (err, content) => {
 if (err) {
   return null;
 }

 console.log(content.toString());
});

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

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

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

Node.js нельзя эффективно использовать, когда требуются сложные вычисления с данными (например, искусственный интеллект, машинное обучение или большие данные), потому что операции блокируют основной (и единственный) поток, делая сервер невосприимчивым. Так было до выпуска Node.js v10.5.0, в котором была добавлена ​​поддержка многопоточности.

Введение:worker_threads

worker_threadsМодули позволяют нам создавать полнофункциональные многопоточные программы Node.js.

Рабочий поток — это фрагмент кода (обычно берется из файла), который порождается в отдельном потоке.

Обратите внимание, что терминthread worker,workerа такжеthreadЧасто используемые взаимозаменяемо, они оба относятся к одному и тому же.

Чтобы использовать рабочие потоки, вы должны импортироватьworker_threadsмодуль. Давайте сначала напишем функцию, которая поможет нам создавать эти рабочие потоки, а затем обсудим их свойства.

type WorkerCallback = (err: any, result?: any) => any;

export function runWorker(path: string, cb: WorkerCallback, workerData: object | null = null) {
 const worker = new Worker(path, { workerData });

 worker.on('message', cb.bind(null, null));
 worker.on('error', cb);

 worker.on('exit', (exitCode) => {
   if (exitCode === 0) {
     return null;
   }

   return cb(new Error(`Worker has stopped with code ${exitCode}`));
 });

 return worker;
}

Чтобы создать работника, вы должны сначала создатьWorkerэкземпляр класса. Его первый параметр указывает путь к файлу, содержащему рабочий код, второй параметр указывает файл с именемworkerDataОбъект, содержащий одно свойство. Это данные, к которым мы хотим, чтобы поток имел доступ при запуске.

Обратите внимание: независимо от того, используете ли вы JavaScript или язык, который вы в конечном итоге конвертируете в JavaScript (например, TypeScript), путь всегда должен указывать на.jsили.mjsфайл расширения.

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

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

Наиболее распространены следующие события:

worker.on('error', (error) => {});

Генерируется всякий раз, когда в рабочем потоке возникает неперехваченное исключение.errorмероприятие. Затем рабочий процесс завершается с ошибкой, доступной в качестве первого аргумента в предоставленном обратном вызове.

worker.on('exit', (exitCode) => {});

Испускается, когда рабочий выходитexitмероприятие. Если призвали в рабочийprocess.exit(),ТакexitCodeбудет предоставлен обратный вызов. Если рабочий начинает сworker.terminate()прекращено, код 1.

worker.on('online', () => {});

Генерируется всякий раз, когда рабочий процесс прекращает синтаксический анализ кода JavaScript и начинает выполнениеonlineмероприятие. Обычно он не используется, но может быть информативным в определенных ситуациях.

worker.on('message', (data) => {});

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

Теперь давайте посмотрим, как данные распределяются между потоками.

обмен данными между потоками

Чтобы отправить данные в другой поток, вы можете использоватьport.postMessage()метод. Его прототип выглядит следующим образом:

port.postMessage(data[, transferList])

Объект порта может бытьparentPort,так же может бытьMessagePortпример — об этом позже.

параметр данных

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

данные поАлгоритм структурированного клонированиясделать копию. Цитата из Мозиллы:

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

Алгоритм не дублирует функции, ошибки, дескрипторы свойств или цепочки прототипов. Также важно отметить, что такое копирование объекта отличается от использования JSON, поскольку оно может содержать циклические ссылки и типизированные массивы, чего не может JSON.

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

Делить память между потоками

люди могут сказать что-то вродеclusterилиchild_processТакие модули давно начали использовать потоки. Это правильно и неправильно.

clusterМодули могут создавать несколько экземпляров узла, при этом запросы маршрутизации основного процесса между ними. Кластеризация может эффективно увеличить пропускную способность сервера, но мы не можем использоватьclusterМодуль порождает отдельный поток.

Люди склонны использовать такие инструменты, как PM2, для централизованного управления своими программами вместо того, чтобы делать это вручную в своем собственном коде. Если вам интересно, посмотрите, как использоватьclusterмодуль.

child_processМодуль может генерировать любой исполняемый файл, независимо от того, написан он на JavaScript или нет. это иworker_threadsОчень похоже, но лишено нескольких важных особенностей последнего.

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

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

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

import { parentPort } from 'worker_threads';

parentPort.on('message', () => {
 const numberOfElements = 100;
 const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * numberOfElements);
 const arr = new Int32Array(sharedBuffer);

 for (let i = 0; i < numberOfElements; i += 1) {
   arr[i] = Math.round(Math.random() * 30);
 }

 parentPort.postMessage({ arr });
});

Во-первых, мы создаемSharedArrayBuffer, его память должна содержать 100 32-битных целых чисел. Затем создайтеInt32ArrayНапример, он будет использовать буфер для хранения своей структуры, затем заполнит массив случайными числами и отправит его родительскому потоку.

В родительском потоке:

import path from 'path';

import { runWorker } from '../run-worker';

const worker = runWorker(path.join(__dirname, 'worker.js'), (err, { arr }) => {
 if (err) {
   return null;
 }

 arr[0] = 5;
});

worker.postMessage({});

Пучокarr [0]Значение изменяется на5, фактически изменит его в два потока.

Конечно, разделяя память, мы рискуем изменить значение в одном потоке, а также изменить его в другом потоке. Но мы также получаем преимущество в этом процессе: значение можно использовать в другом потоке без сериализации, что значительно повышает эффективность. Просто не забудьте управлять правильными ссылками на данные, чтобы они могли быть удалены сборщиком мусора, когда вы закончите их обработку.

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

параметр списка передачи

transferListможет содержать толькоArrayBufferа такжеMessagePort. Как только они доставлены в другой поток, они не могут быть доставлены снова, потому что содержимое памяти уже перемещено в другой поток.

В настоящее время невозможно пройтиtransferList(можно использоватьchild_processmodule) для транспортировки сетевых сокетов.

Создайте каналы связи

Связь между потоками осуществляется через порт, которыйMessagePortЭкземпляры класса и обеспечивают связь на основе событий.

Существует два способа связи между потоками с использованием портов. Первое — это значение по умолчанию, которое является более простым методом. В рабочем коде мы начинаем сworker_threadsМодуль импортирует файл с именемparentPortобъект и использовать объект.postMessage()метод для отправки сообщения в родительский поток.

Вот пример:

import { parentPort } from 'worker_threads';
const data = {
 // ...
};

parentPort.postMessage(data);

parentPortсоздается Node.js за кулисамиMessagePortЭкземпляр, используемый для связи с родительским потоком. так что вы можете использоватьparentPortа такжеworkerОбъекты взаимодействуют между потоками.

Второй способ взаимодействия между потоками — созданиеMessageChannelи отправить работнику. Следующий код показывает, как создать новыйMessagePortи поделитесь им с нашим работником:

import path from 'path';
import { Worker, MessageChannel } from 'worker_threads';

const worker = new Worker(path.join(__dirname, 'worker.js'));

const { port1, port2 } = new MessageChannel();

port1.on('message', (message) => {
 console.log('message from worker:', message);
});

worker.postMessage({ port: port2 }, [port2]);

при созданииport1а такжеport2После этого мыport1установить прослушиватель событий наport2Отправлено рабочему. мы должны включить его вtransferListДля того, чтобы передавать его рабочему.

Внутри рабочего:

import { parentPort, MessagePort } from 'worker_threads';

parentPort.on('message', (data) => {
 const { port }: { port: MessagePort } = data;

 port.postMessage('heres your message!');
});

Таким образом, мы можем использовать порт, отправленный родительским потоком.

использоватьparentPortНе обязательно неправильный путь, но лучше использоватьMessageChannelэкземпляр создает новыйMessagePortА потом поделитесь этим с порожденным работником.

Обратите внимание, что в следующих примерах для краткости я использовалparentPort.

Два способа использования воркеров

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

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

В документации Node.js рекомендуется второй подход, поскольку для создания рабочего потока требуется создание виртуальной машины, а также синтаксический анализ и выполнение кода, что влечет за собой большие накладные расходы. Таким образом, этот метод более эффективен, чем постоянное создание новых рабочих.

Этот подход называется пулом рабочих, потому что мы создаем пул рабочих и заставляем их ждать, отправляя их по мере необходимости.messageсобытия, необходимые для выполнения работы.

Вот пример порождения, выполнения и закрытия воркеров:

import { parentPort } from 'worker_threads';

const collection = [];

for (let i = 0; i < 10; i += 1) {
 collection[i] = i;
}

parentPort.postMessage(collection);

БудуcollectionПосле отправки в родительский поток он завершается.

Вот пример работника, который может долго ждать, прежде чем ему дадут задание:

import { parentPort } from 'worker_threads';

parentPort.on('message', (data: any) => {
 const result = doSomething(data);

 parentPort.postMessage(result);
});

Важные свойства, доступные в модуле worker_threads

worker_threadsВ модулях доступны некоторые свойства:

isMainThread

Когда не работает в рабочем потоке, свойствоtrue. Если вы считаете это необходимым, включите простой в начало рабочего файлаifоператор, чтобы убедиться, что он работает только как рабочий.

import { isMainThread } from 'worker_threads';

if (isMainThread) {
 throw new Error('Its not a worker');
}

workerData

Данные для включения в конструктор рабочего процесса при порождении потока.

const worker = new Worker(path, { workerData });

В рабочем потоке:

import { workerData } from 'worker_threads';

console.log(workerData.property);

parentPort

упоминалось ранееMessagePortЭкземпляр, используемый для связи с родительским потоком.

threadId

Уникальный идентификатор, присвоенный работнику.


Теперь, когда мы знаем технические детали, давайте кое-что реализуем и проверим то, что узнали на практике.

выполнитьsetTimeout

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

import { parentPort, workerData } from 'worker_threads';

const time = Date.now();

while (true) {
    if (time + workerData.time <= Date.now()) {
        parentPort.postMessage({});
        break;
    }
}

Эта конкретная реализация порождает поток и выполняет его код и, наконец, завершает работу после завершения.

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

const timeoutState: { [key: string]: Worker } = {};

Затем функция, отвечающая за создание воркера и сохранение его в состояние:

export function setTimeout(callback: (err: any) => any, time: number) {
 const id = uuidv4();

 const worker = runWorker(
   path.join(__dirname, './timeout-worker.js'),
   (err) => {
     if (!timeoutState[id]) {
       return null;
     }

     timeoutState[id] = null;

     if (err) {
       return callback(err);
     }

     callback(null);
   },
   {
     time,
   },
 );

 timeoutState[id] = worker;

 return id;
}

Сначала мы используем пакет UUID для создания уникального идентификатора для воркера, затем используем ранее определенную функциюrunWorkerчтобы получить рабочих. Мы также передаем функцию обратного вызова воркеру, которая будет запущена, как только воркер отправит данные. Наконец, сохраните работника в состоянии и вернитеid.

В функции обратного вызова мы должны проверить, существует ли рабочий процесс в состоянии, потому что может бытьcancelTimeout(), который удалит его. Если он существует, удалите его из состояния и вызовитеsetTimeoutфункциональныйcallback.

cancelTimeoutиспользование функции.terminate()метод заставляет работника выйти и удаляет работника из состояния:

export function cancelTimeout(id: string) {
 if (timeoutState[id]) {
   timeoutState[id].terminate();

   timeoutState[id] = undefined;

   return true;
 }

 return false;
}

Если вам интересно, я также реализовалsetInterval, код вздесь, но так как он ничего не делает с потоком (мы повторно используемsetTimeoutкод), поэтому я решил не объяснять это здесь.

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

native setTimeout { ms: 7004, averageCPUCost: 0.1416 }
worker setTimeout { ms: 7046, averageCPUCost: 0.308 }

Мы видим, чтоsetTimeoutЕсть небольшая задержка - примерно 40мс - создается потребление Worker. Средняя стоимость ЦП также немного высока, но не является невыносимой (стоимость ЦП — это среднее значение использования ЦП в течение всего процесса).

Если мы сможем повторно использовать воркеры, мы сможем уменьшить задержку и использование ЦП, поэтому мы реализуем пулы воркеров.

Внедрить рабочий пул

Как упоминалось выше, пул воркеров — это заданное количество предварительно созданных воркеров, которые остаются бездействующими и прослушиваютmessageмероприятие. однаждыmessageСобытия запускаются, они начинают работать и возвращают результаты.

Чтобы лучше описать, что мы собираемся делать, давайте создадим пул из восьми рабочих потоков:

const pool = new WorkerPool(path.join(__dirname, './test-worker.js'), 8);

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

Как показано во фрагменте кода выше, мы передаем путь к воркеру и количество воркеров для создания.WorkerPoolконструктор.

export class WorkerPool<T, N> {
 private queue: QueueItem<T, N>[] = [];
 private workersById: { [key: number]: Worker } = {};
 private activeWorkersById: { [key: number]: boolean } = {};

 public constructor(public workerPath: string, public numberOfThreads: number) {
   this.init();
 }
}

Вот некоторые другие свойства, такие какworkersByIdа такжеactiveWorkersById, мы можем сохранить ID существующего воркера и текущего воркера отдельно. а такжеqueue, мы можем использовать следующую структуру для сохранения объекта:

type QueueCallback<N> = (err: any, result?: N) => void;

interface QueueItem<T, N> {
 callback: QueueCallback<N>;
 getData: () => T;
}

callbackПросто обратный вызов узла по умолчанию, первый параметр — это ошибка, а второй параметр — возможные результаты.getDataпередается в рабочий пул.run()Функция метода (описанного ниже), которая будет вызываться после начала обработки элемента.getDataДанные, возвращаемые функцией, будут переданы рабочему потоку.

существует.init()мы создаем воркеры и сохраняем их в следующем состоянии:

private init() {
  if (this.numberOfThreads < 1) {
    return null;
  }

  for (let i = 0; i < this.numberOfThreads; i += 1) {
    const worker = new Worker(this.workerPath);

    this.workersById[i] = worker;
    this.activeWorkersById[i] = false;
  }
}

Чтобы избежать бесконечных циклов, мы сначала убеждаемся, что количество потоков > 1. Затем создайте допустимое количество рабочих и сохраните их индекс вworkersByIdусловие. мы вactiveWorkersByIdСостояние содержит информацию о том, запущены ли они в данный момент или нет, что по умолчанию всегда ложно.

Теперь мы должны реализовать ранее упомянутое.run()метод для установки задач, доступных для работника.

public run(getData: () => T) {
  return new Promise<N>((resolve, reject) => {
    const availableWorkerId = this.getInactiveWorkerId();

    const queueItem: QueueItem<T, N> = {
      getData,
      callback: (error, result) => {
        if (error) {
          return reject(error);
        }
return resolve(result);
      },
    };

    if (availableWorkerId === -1) {
      this.queue.push(queueItem);

      return null;
    }

    this.runWorker(availableWorkerId, queueItem);
  });
}

В функции обещания мы сначала вызываем.getInactiveWorkerId()Чтобы проверить, есть ли незанятые рабочие процессы, доступные для обработки данных:

private getInactiveWorkerId(): number {
  for (let i = 0; i < this.numberOfThreads; i += 1) {
    if (!this.activeWorkersById[i]) {
      return i;
    }
  }

  return -1;
}

Далее мы создаемqueueItem, в котором сохранить переданный.run()методgetDataфункции и обратные вызовы. В обратном вызове мы либоresolveилиrejectобещание, это зависит от того, передал ли работник ошибку обратному вызову или нет.

еслиavailableWorkerIdЗначение равно -1, что означает, что в настоящее время нет доступных рабочих, мы будемqueueItemдобавить вqueue. Если есть свободные рабочие, звоните.runWorker()метод выполнения работника.

существует.runWorker()метод, мы должны поставить текущий рабочийactiveWorkersByIdустановить для использования; дляmessageа такжеerrorСобытие устанавливает прослушиватели событий (и очищает их после); наконец, отправляет данные рабочему процессу.

private async runWorker(workerId: number, queueItem: QueueItem<T, N>) {
 const worker = this.workersById[workerId];

 this.activeWorkersById[workerId] = true;

 const messageCallback = (result: N) => {
   queueItem.callback(null, result);

   cleanUp();
 };

 const errorCallback = (error: any) => {
   queueItem.callback(error);

   cleanUp();
 };

 const cleanUp = () => {
   worker.removeAllListeners('message');
   worker.removeAllListeners('error');

   this.activeWorkersById[workerId] = false;

   if (!this.queue.length) {
     return null;
   }

   this.runWorker(workerId, this.queue.shift());
 };

 worker.once('message', messageCallback);
 worker.once('error', errorCallback);

 worker.postMessage(await queueItem.getData());
}

Во-первых, с помощью переданногоworkerId, мы начинаем сworkersByIdПолучите ссылку на работника в формате . Затем вactiveWorkersByIdв, будет[workerId]Свойство имеет значение true, чтобы мы знали, что, пока рабочий процесс занят, другие задачи не выполняются.

Далее создайтеmessageCallbackа такжеerrorCallbackИспользуется для вызова сообщений и событий ошибок, затем регистрирует функцию для прослушивания событий и отправки данных рабочим процессам.

В обратном вызове мы вызываемqueueItemобратный звонок, затем звонокcleanUpфункция. существуетcleanUpфункцию, чтобы удалить прослушиватель событий, потому что мы будем повторно использовать один и тот же рабочий процесс несколько раз. Если прослушиватель не удалить, произойдет утечка памяти, и память будет медленно исчерпана.

существуетactiveWorkersByIdГосударство, мы будем[workerId]свойство установлено наfalseи проверьте, не пуста ли очередь. Если нет, то изqueueудалить первый элемент и заменить его другимqueueItemПозовите работника еще раз.

Затем создайтеmessageРабочий, который выполняет некоторые вычисления после данных в событии:

import { isMainThread, parentPort } from 'worker_threads';

if (isMainThread) {
 throw new Error('Its not a worker');
}

const doCalcs = (data: any) => {
 const collection = [];

 for (let i = 0; i < 1000000; i += 1) {
   collection[i] = Math.round(Math.random() * 100000);
 }

 return collection.sort((a, b) => {
   if (a > b) {
     return 1;
   }

   return -1;
 });
};

parentPort.on('message', (data: any) => {
 const result = doCalcs(data);

 parentPort.postMessage(result);
});

Рабочий создает массив из 1 миллиона случайных чисел, а затем сортирует их. Неважно, что вы делаете, главное, чтобы это заняло немного больше времени.

Ниже приведен пример простого использования рабочего пула:

const pool = new WorkerPool<{ i: number }, number>(path.join(__dirname, './test-worker.js'), 8);

const items = [...new Array(100)].fill(null);

Promise.all(
 items.map(async (_, i) => {
   await pool.run(() => ({ i }));

   console.log('finished', i);
 }),
).then(() => {
 console.log('finished all');
});

Сначала создайте рабочий пул, состоящий из восьми рабочих. Затем создаем массив из 100 элементов, для каждого элемента запускаем задачу в worker pool. Восемь задач выполняются сразу после запуска, а остальные задачи ставятся в очередь и выполняются одна за другой. Используя пул рабочих процессов, нам не нужно каждый раз создавать одного рабочего, что значительно повышает эффективность.

В заключение

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

Добро пожаловать на официальный аккаунт Jingcheng Yideng: Jingcheng Yideng, чтобы получить больше галантереи.