JavaScript и конкуренция: потоки, горутины и async/await без головной боли

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

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

Начать курс

Один тред, чтобы править всеми: мифы и правда о конкурентности в JavaScript

Когда-то давно, в эпоху статичных сайтов и кнопок «Отправить», которые блокировали весь интерфейс, JavaScript жил счастливо в одном потоке. Ему было достаточно: пришёл запрос — выполнился, ушёл запрос — интерфейс замер. Но времена изменились. Сегодня мы ждём от веб-приложений мгновенной реакции: скролл не должен тормозить, анимации — плавные, а загрузка данных — незаметной. И тут в игру вступает конкурентность.

Многие разработчики путают конкурентность (concurrency) с параллелизмом (parallelism). В JavaScript, особенно в браузере, мы чаще имеем дело с первым. Это как жонглёр: он не держит все мячи в воздухе одновременно (это параллелизм), но он так ловко переключается между ними, что создаётся иллюзия одновременности. В этой статье мы разберём три ключевых инструмента для организации конкурентности в JS: async/await, горутины (через генераторы и Web Workers) и классические потоки. Поехали!

Async/Await: элегантный асинхронный код

async/await — это синтаксический сахар над промисами (Promises), который появился в ES2017. Он не делает код многопоточным, но делает его асинхронным. Когда вы пишете await, JavaScript «засыпает» в этой функции, освобождая поток для других задач. Как только промис разрешается, функция просыпается и продолжает выполнение. Это идеальный инструмент для I/O операций: запросы к API, чтение файлов, таймеры.

Простой пример: загрузка данных с сервера

// Функция, которая возвращает Promise
function fetchUser(id) {
    return new Promise((resolve) => {
        setTimeout(() => resolve({ id, name: 'Алексей' }), 1000);
    });
}

// Асинхронная функция
async function loadUserProfile(userId) {
    console.log('Начинаем загрузку...');
    const user = await fetchUser(userId);
    console.log('Пользователь загружен:', user.name);
    return user;
}

loadUserProfile(42);
console.log('Этот код выполнится сразу, не дожидаясь загрузки!');

Обратите внимание: console.log('Этот код выполнится сразу...') сработает раньше, чем console.log('Пользователь загружен'). Поток не блокируется — это и есть конкурентность в действии.

Обработка ошибок

async function fetchWithRetry(url, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error('Ошибка HTTP');
            return await response.json();
        } catch (error) {
            if (i === retries - 1) throw error;
            console.log(`Попытка ${i + 1} не удалась, пробуем снова...`);
        }
    }
}

Когда использовать: любые операции с внешними ресурсами (API, базы данных, файловая система в Node.js). async/await — ваш основной инструмент для асинхронного кода.

Горутины в JavaScript: миф или реальность?

Термин «горутина» пришёл из Go и обозначает легковесный поток выполнения. В JavaScript нет нативных горутин, но есть генераторы (Generator Functions) и Web Workers, которые могут имитировать подобное поведение. Генераторы позволяют приостанавливать и возобновлять выполнение функции, что даёт контроль над потоком исполнения.

Генераторы как основа конкурентности

function* taskGenerator() {
    console.log('Запуск задачи 1');
    yield 'Задача 1 завершена';
    console.log('Запуск задачи 2');
    yield 'Задача 2 завершена';
    console.log('Запуск задачи 3');
    yield 'Задача 3 завершена';
}

const iterator = taskGenerator();
console.log(iterator.next().value); // Выполняет задачу 1
// В этот момент можно сделать что-то ещё
console.log(iterator.next().value); // Выполняет задачу 2
console.log(iterator.next().value); // Выполняет задачу 3

Генераторы не дают настоящей параллельности, но они идеальны для кооперативной многозадачности. Вы сами решаете, когда передать управление. В сочетании с промисами генераторы превращаются в мощный инструмент, на котором, кстати, был построен ранний async/await (через co библиотеку).

Web Workers: настоящая многопоточность

Если вам нужно выполнить тяжёлые вычисления (например, обработка изображений или криптография) без блокировки UI, Web Workers — ваш выбор. Они запускаются в отдельном потоке и общаются с основным через сообщения.

// main.js
const worker = new Worker('worker.js');
worker.postMessage({ data: [1, 2, 3, 4, 5] });

worker.onmessage = (event) => {
    console.log('Результат из воркера:', event.data);
};

// worker.js
self.onmessage = (event) => {
    const result = event.data.data.map(x => x * x);
    self.postMessage(result);
};

Важно: Web Workers не имеют доступа к DOM, но они идеальны для CPU-интенсивных задач. В Node.js аналогом являются Worker Threads.

Event Loop: дирижёр оркестра конкурентности

Чтобы понять, как всё это работает, нужно заглянуть под капот — в Event Loop. Это механизм, который бесконечно проверяет стек вызовов и очереди задач. Когда стек пуст, Event Loop берёт задачу из очереди макрозадач (setTimeout, fetch) или микрозадач (Promise.then, queueMicrotask) и помещает её в стек.

Вот упрощённая схема:

  1. Выполняется синхронный код.
  2. Микрозадачи (Promise) выполняются сразу после синхронного кода.
  3. Макрозадачи (setTimeout) — в следующем цикле.
console.log('1: Синхронный код');

setTimeout(() => console.log('2: setTimeout'), 0);

Promise.resolve().then(() => console.log('3: Promise'));

console.log('4: Ещё синхронный код');

// Вывод: 1, 4, 3, 2

Это знание помогает избежать ошибок: если вы поместите тяжёлую синхронную операцию (например, цикл на миллион итераций), Event Loop застынет и интерфейс «замрёт». Решение — разбить задачу на части через setTimeout или передать в Worker.

Сравнение подходов: когда что использовать?

Давайте сведём всё в таблицу (мысленно, так как мы в HTML):

  • Async/Await + Promises — для I/O операций (сеть, файлы, базы данных). Легко читается, поддерживается всеми современными средами.
  • Генераторы — для слож

Блоги

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