Конкурентность и многопоточность в JavaScript: продвинутый уровень

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Конкурентность и многопоточность в JavaScript: продвинутый уровень

JavaScript исторически был однопоточным языком, работающим в рамках цикла событий (Event Loop). Однако современные приложения на Node.js требуют высокой производительности при обработке CPU-интенсивных задач. В этой статье мы разберем продвинутые механизмы конкурентности и многопоточности: Worker Threads, SharedArrayBuffer, Atomics и кластеризацию. Вы узнаете, как эффективно использовать многоядерные процессоры и избегать блокировки основного потока.

1. Worker Threads: настоящая многопоточность в Node.js

Модуль worker_threads появился в Node.js 10.5.0 и позволяет создавать отдельные потоки с собственным V8-движком. В отличие от кластеризации (процессы), Worker Threads разделяют память и могут обмениваться данными через SharedArrayBuffer.

Создание и запуск воркера

// main.js
const { Worker } = require('worker_threads');

function runWorker(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { workerData: data });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}

(async () => {
  const result = await runWorker({ numbers: [1, 2, 3, 4, 5] });
  console.log('Результат:', result); // { sum: 15 }
})();
// worker.js
const { parentPort, workerData } = require('worker_threads');

const sum = workerData.numbers.reduce((acc, val) => acc + val, 0);
parentPort.postMessage({ sum });

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

2. SharedArrayBuffer и Atomics: разделяемая память

SharedArrayBuffer позволяет нескольким воркерам читать и записывать данные в одну область памяти. Для синхронизации доступа используется объект Atomics — он гарантирует атомарные операции.

Пример: параллельная обработка массива

// main.js
const { Worker } = require('worker_threads');
const { SharedArrayBuffer } = require('worker_threads');

const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 10);
const array = new Int32Array(buffer);

// Заполняем массив
for (let i = 0; i < 10; i++) array[i] = i;

const worker1 = new Worker('./worker_atomics.js', { workerData: { start: 0, end: 5 } });
const worker2 = new Worker('./worker_atomics.js', { workerData: { start: 5, end: 10 } });

worker1.on('message', () => console.log('Worker 1 завершил'));
worker2.on('message', () => console.log('Worker 2 завершил'));
// worker_atomics.js
const { parentPort, workerData } = require('worker_threads');
const { Atomics } = require('worker_threads');

const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 10);
const array = new Int32Array(buffer);

// Атомарное увеличение каждого элемента
for (let i = workerData.start; i < workerData.end; i++) {
  Atomics.add(array, i, 10);
}

parentPort.postMessage('done');

Примечание: Используйте Atomics.wait() и Atomics.notify() для синхронизации потоков (аналог мьютексов). Это критично для предотвращения состояний гонки.

3. Кластеризация: масштабирование через процессы

Модуль cluster создает несколько процессов Node.js (воркеров), каждый из которых обрабатывает запросы. Это стандартный способ для масштабирования HTTP-серверов на многоядерных системах.

Пример: кластеризованный HTTP-сервер

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

if (cluster.isMaster) {
  console.log(`Master ${process.pid} is running`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork(); // Автоматически перезапускаем
  });
} else {
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello from worker ' + process.pid);
  }).listen(8000);

  console.log(`Worker ${process.pid} started`);
}

Важно: Каждый воркер — отдельный процесс с собственной памятью. Для обмена данными используйте Redis, RabbitMQ или файловую систему. Кластеризация идеальна для REST API, веб-сокетов и прокси.

4. Продвинутые паттерны: пул воркеров и очереди

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

Реализация пула воркеров

const { Worker } = require('worker_threads');
const { EventEmitter } = require('events');

class WorkerPool extends EventEmitter {
  constructor(workerPath, poolSize = 4) {
    super();
    this.workers = [];
    this.queue = [];
    this.activeJobs = 0;

    for (let i = 0; i < poolSize; i++) {
      const worker = new Worker(workerPath);
      worker.on('message', (msg) => this._handleResult(worker, msg));
      worker.on('error', (err) => this.emit('error', err));
      this.workers.push({ worker, busy: false });
    }
  }

  runTask(data) {
    return new Promise((resolve, reject) => {
      const task = { data, resolve, reject };
      const freeWorker = this.workers.find(w => !w.busy);

      if (freeWorker) {
        this._assignTask(freeWorker, task);
      } else {
        this.queue.push(task);
      }
    });
  }

  _assignTask(workerObj, task) {
    workerObj.busy = true;
    workerObj.worker.postMessage(task.data);
    workerObj.currentTask = task;
    this.activeJobs++;
  }

  _handleResult(worker, msg) {
    const workerObj = this.workers.find(w => w.worker === worker);
    workerObj.busy = false;
    workerObj.currentTask.resolve(msg);
    this.activeJobs--;

    if (this.queue.length > 0) {
      this._assignTask(workerObj, this.queue.shift());
    }
  }
}

module.exports = WorkerPool;

Использование:

const pool = new WorkerPool('./worker.js', 4);

for (let i =                

Блоги

Рекомендаций по книгам