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

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

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

Начать курс

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

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

В этой статье мы разберём, чем отличается конкурентность от параллелизма, как работает Event Loop, когда использовать Worker Threads и Web Workers, а также рассмотрим практические примеры на Node.js.

1. Event Loop и неблокирующий ввод-вывод

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

Представьте себе очередь: Event Loop берёт задачу из очереди (call stack), выполняет её, и если встречает асинхронную операцию (например, setTimeout или fetch), передаёт её в Web API (в браузере) или libuv (в Node.js). После завершения операции её колбэк попадает в очередь задач (task queue). Event Loop постоянно проверяет, не пуст ли стек, и, если он пуст, берёт следующую задачу из очереди.

Пример асинхронного кода:

const fs = require('fs');

console.log('Начало');

fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) throw err;
    console.log('Файл прочитан:', data);
});

console.log('Конец');

// Вывод:
// Начало
// Конец
// Файл прочитан: ...

Обратите внимание: console.log('Конец') выполняется до чтения файла. Это и есть неблокирующий ввод-вывод — основной поток не ждёт завершения операции, а продолжает выполнение.

2. Web Workers и Worker Threads: настоящая многопоточность

Хотя Event Loop отлично справляется с I/O-операциями, он не помогает при выполнении "тяжёлых" вычислений (обработка изображений, криптография, сложные сортировки). Если запустить такой код в основном потоке, весь интерфейс (или сервер) "зависнет" до завершения вычислений.

Для решения этой проблемы существуют Web Workers (в браузере) и Worker Threads (в Node.js). Они позволяют запускать JavaScript-код в отдельных потоках операционной системы.

Пример с Worker Threads в Node.js

Создадим файл worker.js:

const { parentPort } = require('worker_threads');

// Тяжёлая задача: вычисление суммы чисел
let sum = 0;
for (let i = 0; i < 1e9; i++) {
    sum += i;
}

// Отправляем результат обратно в основной поток
parentPort.postMessage(sum);

Основной файл main.js:

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

console.log('Основной поток: запуск воркера');

const worker = new Worker('./worker.js');

worker.on('message', (result) => {
    console.log('Результат от воркера:', result);
});

worker.on('error', (err) => {
    console.error('Ошибка воркера:', err);
});

worker.on('exit', (code) => {
    if (code !== 0) {
        console.error(`Воркер завершился с кодом ${code}`);
    }
});

console.log('Основной поток: продолжаем работу...');

// Вывод:
// Основной поток: запуск воркера
// Основной поток: продолжаем работу...
// Результат от воркера: 499999999500000000

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

3. Async/Await и Promise: управление конкурентностью

Современный JavaScript предлагает элегантные способы работы с асинхронным кодом — async/await и Promise. Они не создают новые потоки, но позволяют писать асинхронный код так, будто он синхронный.

Пример параллельного выполнения (concurrency):

async function fetchData() {
    const promise1 = fetch('https://api.example.com/data1');
    const promise2 = fetch('https://api.example.com/data2');
    const promise3 = fetch('https://api.example.com/data3');

    // Запускаем все три запроса одновременно
    const results = await Promise.all([promise1, promise2, promise3]);
    
    return results.map(res => res.json());
}

fetchData()
    .then(data => console.log('Все данные получены:', data))
    .catch(err => console.error('Ошибка:', err));

Здесь три запроса выполняются конкурентно (но не параллельно — они используют один поток и Event Loop). Это идеально для I/O-задач.

4. Когда использовать конкурентность, а когда — многопоточность?

Выбор между конкурентностью (Event Loop + async/await) и многопоточностью (Worker Threads) зависит от типа задач:

  • I/O-задачи (чтение файлов, HTTP-запросы, работа с БД) — используйте async/await и Promise.all. Event Loop справится отлично.
  • CPU-интенсивные задачи (обработка изображений, криптография, парсинг больших JSON) — используйте Worker Threads или Web Workers, чтобы не блокировать основной поток.
  • Смешанные сценарии — комбинируйте оба подхода. Например, основной поток управляет логикой и I/O, а воркеры выполняют тяжёлые вычисления.

Важно: Не создавайте слишком много воркеров — каждый из них потребляет память и ресурсы ОС. Оптимально — 2-4 воркера на ядро процессора.

Заключение

JavaScript (и Node.js) предоставляет мощные инструменты для работы с конкурентностью и многопоточностью. Event Loop с async/await идеально подходит для неблокирующего I/O, а Worker Threads и Web Workers — для параллельных вычислений.

Помните главное правило: не пытайтесь сделать всё в одном потоке. Если ваше приложение выполняет тяжёлые расчёты или обрабатывает много запросов — используйте воркеры. Если вы

Блоги

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