Конкурентность и параллелизм: в чём разница
Основы4 минРедакция Innovialabs
Коротко
Конкурентность — это умение вести несколько задач одновременно, переключаясь между ними; параллелизм — это буквальное выполнение нескольких задач в один момент на разных ядрах.
Короткий ответ
Конкурентность (concurrency) — это про структуру программы: она умеет держать в работе несколько задач и переключаться между ними, пока одна чего-то ждёт.
Параллелизм (parallelism) — это про исполнение: несколько задач в буквальном смысле выполняются в один и тот же момент на разных ядрах процессора.
Конкурентная программа может работать на одном ядре. Параллельная требует нескольких ядер. Одно не исключает другого: хорошо устроенная система часто и конкурентна, и параллельна.
Аналогия с кухней
Представьте одного повара. Он ставит воду на огонь, пока она закипает — режет овощи, затем помешивает соус. В каждый момент он делает что-то одно, но три блюда продвигаются вперёд. Это конкурентность.
Теперь на кухне три повара, и каждый готовит своё блюдо. Работа идёт буквально одновременно. Это параллелизм.
Ключевой вывод: если повар в основном ждёт (вода закипает, духовка печёт), второй повар почти не поможет — достаточно грамотно переключаться. Если повар занят руками всё время (нарезка горы овощей), поможет только ещё одна пара рук.
Два типа нагрузки
От типа нагрузки зависит, какой инструмент выбрать.
- I/O-bound — задача в основном ждёт: ответ базы данных, HTTP-запрос, чтение файла, сообщение из очереди. Процессор при этом простаивает.
- CPU-bound — задача в основном считает: обработка изображений, сжатие, шифрование, расчёты, обучение моделей.
Для I/O-bound нагрузки нужна конкурентность. Для CPU-bound — параллелизм.
Потоки, процессы и async
| Механизм | Что это | Сильная сторона | Слабая сторона |
|---|---|---|---|
| Процессы | Отдельные программы со своей памятью | Настоящий параллелизм, изоляция | Дороже по памяти, обмен данными сложнее |
| Потоки (threads) | Несколько потоков исполнения внутри одного процесса | Общая память, удобно для ожидания I/O | Гонки данных, нужны блокировки |
| Async I/O | Одна петля событий (event loop) переключает задачи в точках ожидания | Тысячи одновременных соединений при малых затратах | Блокирующий код останавливает всё; нужны async-библиотеки |
Async — чистая конкурентность: задачи сами отдают управление на await, и петля событий запускает следующую. Это эффективно, пока никто не занимает процессор надолго.
Что такое GIL в Python
В стандартной реализации CPython есть GIL (Global Interpreter Lock) — глобальная блокировка, которая позволяет только одному потоку выполнять Python-байткод в каждый момент времени.
Что это значит на практике:
- Потоки в Python хорошо подходят для I/O-bound задач: во время ожидания сети или диска GIL отпускается, и другие потоки работают.
- Потоки не ускоряют CPU-bound код на чистом Python: вычисления всё равно идут по очереди.
- Для параллельных вычислений используют процессы (
multiprocessing,ProcessPoolExecutor) или библиотеки, которые выполняют тяжёлую работу вне GIL (например, многие функции NumPy).
В новых версиях Python появилась экспериментальная сборка без GIL, но в большинстве проектов пока стоит исходить из того, что GIL есть.
import asyncio
from concurrent.futures import ProcessPoolExecutor
async def fetch_all(client, urls):
# I/O-bound: конкурентность через async
return await asyncio.gather(*(client.get(u) for u in urls))
def heavy(n):
return sum(i * i for i in range(n))
def compute_all(numbers):
# CPU-bound: параллелизм через процессы
with ProcessPoolExecutor() as pool:
return list(pool.map(heavy, numbers))
Как выбрать подход
- Определите, чего ждёт задача. Если сети или диска — начните с async или потоков.
- Много соединений и современный стек (FastAPI, aiohttp, aiogram) — async.
- Старые синхронные библиотеки и умеренная нагрузка — пул потоков.
- Тяжёлые вычисления — пул процессов или вынос работы в отдельный сервис или очередь задач.
- Смешанная нагрузка — async для I/O, а тяжёлые куски отдавайте в пул процессов через
run_in_executor.
Частые ошибки
- Блокирующий вызов внутри async-кода.
time.sleepили синхронный драйвер базы данных замораживает всю петлю событий. - Потоки для расчётов на Python в расчёте на ускорение — из-за GIL его не будет.
- Слишком много процессов. Больше, чем ядер, обычно лишь добавляет накладные расходы.
- Общие изменяемые данные без защиты. Гонки проявляются редко и непредсказуемо, поэтому их сложно отлаживать.