Конкурентность и параллелизм
Это первый урок второй фазы раздела. В первой фазе мы разобрали контракты: как значение приходит, в каких порциях, кто двигает поток. Теперь поднимаемся на уровень выше и смотрим на саму модель исполнения. Два слова, которые в разговорах постоянно путают, это “конкурентно” и “параллельно”. Путаница не безобидная: из неё растут неверные оптимизации, лишние worker-ы там, где хватило бы одного потока, и наоборот, надежда на event loop там, где он бессилен.
Разведём их раз и навсегда, а потом проверим на живой шкале времени.
1. Три слова, которые не синонимы
Сначала короткие определения, к ним будем возвращаться весь урок.
- Асинхронность, это про способ записи: операция не отдаёт результат сразу, а обещает его потом.
- Конкурентность, это про структуру: несколько задач находятся в работе разом, их шаги чередуются.
- Параллелизм, это про исполнение: несколько вычислений физически идут в один момент на разных ядрах.
Главная мысль урока: это три ортогональные оси. Можно быть конкурентным без параллелизма (один поток с event loop). Можно быть параллельным без особой конкурентной структуры (numpy перемножает матрицу на всех ядрах, а код выглядит как одна строка). И асинхронность это вообще про синтаксис, она не обещает ни того, ни другого.
Роб Пайк формулирует так: конкурентность это про то, как ты раскладываешь задачу на независимые части, а параллелизм это про то, как эти части исполняются. Хорошая конкурентная структура позволяет параллелизм, если исполнителей несколько, но не требует его.
2. Конкурентность это структура
Представь одного бариста. К нему стоит очередь из пяти человек. Он не варит пять кофе одновременно: рук у него две, кофемашина одна. Но он работает конкурентно. Поставил чашку под пролив, и пока льётся эспрессо, принимает заказ у следующего, пробивает чек у третьего. Один исполнитель, но несколько заказов в работе. Шаги чередуются.
В коде это ровно event loop. Один поток JavaScript, но в любой момент “в полёте” десятки запросов: один ждёт ответ базы, другой читает файл, третий считает ответ. Пока один ждёт ввод-вывод, поток занят другим.
// Три запроса в работе одновременно, но поток один
const [users, posts, stats] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchStats(),
]);
Promise.all не создаёт потоков. Он лишь говорит: запусти три ожидания, и
пока все три не ответят, занимайся чем угодно. Конкурентность есть, а
параллелизма нет: считать что-то на CPU поток всё равно может только по
одной задаче за раз.
3. Параллелизм это исполнение
Теперь представь кофейню с тремя баристами и тремя кофемашинами. Три кофе готовятся физически одновременно. Это параллелизм: несколько исполнителей, несколько вычислений в один момент.
В Node за это отвечают не промисы, а отдельные потоки исполнения:
worker_threads, cluster, дочерние процессы. У каждого свой
V8 isolate,
своя куча, свой event loop.
import { Worker } from 'node:worker_threads';
// Тяжёлый расчёт уезжает в отдельный поток, основной свободен
const worker = new Worker('./hash-worker.js', { workerData: buffer });
worker.on('message', (digest) => console.log(digest));
Пока worker считает хэш, основной поток продолжает принимать запросы. Вот теперь у нас две вещи сразу: и конкурентная структура (несколько задач в работе), и параллельное исполнение (два потока реально работают в один момент).
4. Один поток конкурентен, но не параллелен
Соберём это на шкале времени. Возьмём две задачи. Каждая немного считает на CPU, потом ждёт сеть, потом дочитывает. Один и тот же набор работ прогоним в трёх режимах и сравним, сколько займёт по часам.
Что важно прочитать на этой шкале:
- Последовательно: один поток, и ожидание сети блокирует его целиком. Пока задача A ждёт ответ, никто не работает. Время равно сумме всех отрезков. Так ведёт себя синхронный блокирующий код.
- Конкурентно: тот же один поток, но ожидание не блокирует. Пока A ждёт сеть, на процессоре крутится B. Это и есть event loop. Время резко падает, хотя ядро всё ещё одно. Маленькая дырка простоя честно показывает: одно ядро остаётся узким местом, когда обеим задачам нужен процессор одновременно.
- Параллельно: два потока, два ядра. A и B стартуют разом. Здесь время ещё чуть меньше, но выигрыш над конкурентностью крошечный.
Последний пункт это и есть пуанта. Наша задача упирается в сеть, а не в процессор. Конкурентность уже отлично прячет ожидание ввода-вывода, и добавлять потоки почти бесполезно: ядра простаивали бы во время ожидания.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.