Перейти к содержанию
Innovialabs

Конкурентность и параллелизм: в чём разница

Основы4 мин

Коротко

Конкурентность — это умение вести несколько задач одновременно, переключаясь между ними; параллелизм — это буквальное выполнение нескольких задач в один момент на разных ядрах.

Короткий ответ

Конкурентность (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))

Как выбрать подход

  1. Определите, чего ждёт задача. Если сети или диска — начните с async или потоков.
  2. Много соединений и современный стек (FastAPI, aiohttp, aiogram) — async.
  3. Старые синхронные библиотеки и умеренная нагрузка — пул потоков.
  4. Тяжёлые вычисления — пул процессов или вынос работы в отдельный сервис или очередь задач.
  5. Смешанная нагрузка — async для I/O, а тяжёлые куски отдавайте в пул процессов через run_in_executor.

Частые ошибки

  • Блокирующий вызов внутри async-кода. time.sleep или синхронный драйвер базы данных замораживает всю петлю событий.
  • Потоки для расчётов на Python в расчёте на ускорение — из-за GIL его не будет.
  • Слишком много процессов. Больше, чем ядер, обычно лишь добавляет накладные расходы.
  • Общие изменяемые данные без защиты. Гонки проявляются редко и непредсказуемо, поэтому их сложно отлаживать.

Частые вопросы

Да. Async-сервер на одном ядре обслуживает тысячи соединений, переключаясь между ними, но в каждый момент выполняет только одну задачу.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.