Многопоточность True Node

Node.js

Что такое однопоточность Node? Что случилось с многопоточностью Node? Надеюсь, эта статья прояснит ситуацию.

Время чтения составляет около 10 ~ 13 минут

В этой статье тестируется среда использования:
Система: macOS Мохаве 10.14.2
ЦП: 4 ядра 2,3 ГГц
Node: 10.15.1

Начните с потоков Node.

Большинство людей понимают, что Node является однопоточным, поэтому после запуска Node количество потоков должно быть равно 1. Давайте проведем эксперимент, чтобы убедиться в этом.

setInterval(() => {
  console.log(new Date().getTime())
}, 3000)

Вы можете видеть, что процесс Node занимает 7 потоков. Почему 7 тем?

Все мы знаем, что ядром Node является движок v8.После запуска Node будет создан экземпляр v8.Этот экземпляр является многопоточным.

  • Основной поток: компиляция и выполнение кода.
  • Компиляция/оптимизация потока: код можно оптимизировать во время выполнения основного потока.
  • Поток профилировщика: записывает время выполнения профилированного кода и предоставляет Crankshaft основу для оптимизации выполнения кода.
  • Несколько потоков сборки мусора.

Поэтому часто говорят, что Node является однопоточным, что означает, что выполнение JavaScript является однопоточным, но среда размещения JavaScript, будь то Node или браузер, многопоточная.

В Node есть два компилятора:
full-codegen: Быстро и легко компилируйте js в простой, но медленный машинный код.
Crankshaft: более сложный оптимизирующий компилятор в реальном времени, который компилирует высокопроизводительный исполняемый код.

Некоторые асинхронные операции ввода-вывода занимают дополнительные потоки.

В приведенном выше примере мы читаем файл во время работы таймера:

const fs = require('fs')

setInterval(() => {
    console.log(new Date().getTime())
}, 3000)

fs.readFile('./index.html', () => {})

Количество потоков стало 11, это связано с тем, что в Node есть некоторые операции ввода-вывода (DNS, FS) и некоторые вычисления с интенсивным использованием ЦП (Zlib, Crypto), которые включают пул потоков Node, а размер пула потоков по умолчанию равен 4, потому что пул потоков Число становится 11.

Мы можем вручную изменить размер пула потоков по умолчанию:

process.env.UV_THREADPOOL_SIZE = 64

Одна строка кода легко превращает потоки в 71.

Является ли кластер многопоточным?

Одиночный поток Node также вызывает некоторые проблемы, такие как недозагрузка процессора, неперехваченное исключение может привести к завершению работы всей программы и т. д. Поскольку модуль кластера предоставляется в Node, кластер реализует инкапсуляцию child_process и реализует модель с несколькими процессами, создавая дочерние процессы с помощью метода fork. Например, наиболее часто используемый pm2 является лучшим представителем.

Давайте посмотрим на демонстрацию кластера:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程可以共享任何 TCP 连接。
  // 在本例子中,共享的是 HTTP 服务器。
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello World');
  }).listen(8000);
  console.log(`工作进程 ${process.pid} 已启动`);
}

На этот раз взгляните на Activity Monitor:

Всего имеется 9 процессов, один из которых является основным процессом, количество процессоров x количество ядер процессора = 2 x 4 = 8 дочерних процессов.

Итак, будь то child_process или cluster, это не многопоточная модель, а многопроцессная модель. Хотя разработчики знают о проблеме однопоточной модели, они принципиально не решают проблему и предоставляют многопроцессный способ имитации многопоточности. Как видно из предыдущих экспериментов, хотя Node (V8) сам по себе имеет возможность многопоточности, разработчики не могут эффективно использовать эту возможность, и нижний уровень Node предоставляет дополнительные способы использования многопоточности. Официальный представитель узла сказал:

Вы можете использовать встроенный пул рабочих узлов Node, разработав надстройку C++. В более старых версиях Node создавайте надстройку C++ с помощью NAN, а в более новых версиях используйте N-API. node-webworker-threads предлагает способ только для JavaScript получить доступ к пулу рабочих узлов.

Но для разработчиков JavaScript никогда не существовало стандартного, простого в использовании способа использования возможностей многопоточности Node.

true — многопоточность узла

До выхода Node 10.5.0 официалы давали экспериментальный модульworker_threadsПредоставляет настоящие многопоточные возможности для Node.

Давайте посмотрим на простую демонстрацию:

const {
  isMainThread,
  parentPort,
  workerData,
  threadId,
  MessageChannel,
  MessagePort,
  Worker
} = require('worker_threads');

function mainThread() {
  for (let i = 0; i < 5; i++) {
    const worker = new Worker(__filename, { workerData: i });
    worker.on('exit', code => { console.log(`main: worker stopped with exit code ${code}`); });
    worker.on('message', msg => {
      console.log(`main: receive ${msg}`);
      worker.postMessage(msg + 1);
    });
  }
}

function workerThread() {
  console.log(`worker: workerDate ${workerData}`);
  parentPort.on('message', msg => {
    console.log(`worker: receive ${msg}`);
  }),
  parentPort.postMessage(workerData);
}

if (isMainThread) {
  mainThread();
} else {
  workerThread();
}

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

Поскольку worker_thread все еще находится в экспериментальной стадии, его необходимо увеличить при запуске.--experimental-workerфлаг, смотреть Activity Monitor после запуска:

Не больше, не меньше, просто еще пять подпотоков.

worker_threadмодуль

worker_thread основной код

В модуле worker_thread есть 4 объекта и 2 класса.

  • isMainThread: Является ли это основным потоком, исходный код проходит черезthreadId === 0судить.
  • MessagePort: используется для связи между потоками, унаследованными от EventEmitter.
  • MessageChannel: экземпляр канала для создания асинхронной двусторонней связи.
  • threadId: идентификатор потока.
  • Рабочий: используется для создания дочерних потоков в основном потоке. Первый параметр — это имя файла, которое представляет запись выполнения дочернего потока.
  • parentPort: объект типа MessagePort, представляющий родительский процесс в рабочем потоке, null в основном потоке.
  • workerData: используется для передачи данных (копии данных) в основном процессе дочернему процессу.

Давайте посмотрим на пример процесса коммуникации:

const assert = require('assert');
const {
  Worker,
  MessageChannel,
  MessagePort,
  isMainThread,
  parentPort
} = require('worker_threads');
if (isMainThread) {
  const worker = new Worker(__filename);
  const subChannel = new MessageChannel();
  worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]);
  subChannel.port2.on('message', (value) => {
    console.log('received:', value);
  });
} else {
  parentPort.once('message', (value) => {
    assert(value.hereIsYourPort instanceof MessagePort);
    value.hereIsYourPort.postMessage('the worker is sending this');
    value.hereIsYourPort.close();
  });
}

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

Многопроцессорность против многопоточности

Согласно утверждению в университетском учебнике: «Процесс — наименьшая единица распределения ресурсов, а поток — наименьшая единица планирования ЦП». выбирать разумно в соответствии с нашими потребностями.

Давайте сравним многопоточность и многопроцессорность:

Атрибуты мультипрогресс Многопоточность сравнивать
данные Обмен данными сложен и требует IPC; данные разделены, а синхронизация проста Поскольку данные процесса являются общими, обмен данными прост, а синхронизация сложна. У каждого свои достоинства
процессор, память Занимает много памяти, переключение затруднено, а загрузка процессора низкая. Низкое использование памяти, простое переключение и высокая загрузка ЦП Многопоточность лучше
уничтожить, переключиться Создание и уничтожение, переключение сложно, а скорость низкая Создание и уничтожение, переключение просто и быстро Многопоточность лучше
coding Простое кодирование и легкая отладка Кодирование и отладка сложны многопроцессорность лучше
надежность Процессы выполняются независимо и не влияют друг на друга Нить разделяет та же участь, что и дыхание многопроцессорность лучше
распределенный Может использоваться для многомашинного многоядерного распределенного, легко расширяемого Может использоваться только для многоядерных распределенных многопроцессорность лучше

Приведенные выше сравнения представляют собой только общие ситуации и не являются абсолютными.

work_thread дает Node настоящие возможности многопоточности, что немаловажно.

Перейдите на официальный аккаунт [Сообщество IVWEB], чтобы увидеть больше статей о галантерейных товарах.