threading (многопоточность)
Модель: один процесс, несколько потоков. Все потоки разделяют память процесса: общие объекты, общий heap (куча). Планирование потоков делает ОС (preemptive): она сама решает, когда переключаться между потоками.
Особенности в CPython:
Есть GIL: одновременно байткод реально исполняет только один поток, остальные ждут.
В итоге нет настоящего параллелизма для чисто вычислительных задач, но при блокирующем I/O поток отпускает GIL, другие потоки могут работать. Для I/O‑bound задач (много ожиданий сети/диска) многопоточность даёт выигрыш.
Плюсы:
Легко прикрутить к старому синхронному коду и к библиотекам, которые не умеют работать асинхронно (
async).
Минусы:
GIL убивает параллелизм для CPU‑bound.
Нужны блокировки/синхронизация при доступе к общим данным.
Потоки «тяжелее» задач в asyncio (больше накладных расходов).
multiprocessing (многопроцессность)
Модель: несколько процессов, каждый со своим интерпретатором, своей памятью и своим GIL. ОС реально выполняет процессы на разных ядрах → настоящий параллелизм для CPU‑bound задач.
Особенности:
Процессы не разделяют память по умолчанию: данные передаются через очереди, пайпы, shared memory, что требует сериализации (pickle и т.п.). Также создание процессов дороже, чем создание потоков, больше накладных расходов на взаимодействие.
Плюсы:
Реально задействует несколько ядер для тяжёлых вычислений.
GIL перестаёт быть проблемой (у каждого процесса свой).
Минусы:
Более тяжёлый старт и коммуникация между процессами.
Сложнее отлаживать, особенно на Windows со spawn.
asyncio (асинхронность / event loop)
Модель: один процесс, один поток, один event loop. Много корутин (async‑функций), которые кооперативно делят время: отдают управление, когда делают await. Как работает: Event loop запускает корутины и переключается между ними только в точках await. Пока корутина ждёт (сетевой запрос, таймер, БД), event loop выполняет другие корутины. GIL никуда не исчезает, просто код почти всё время ждёт I/O, а не считает CPU.
Плюсы:
Очень маленькие накладные расходы на задачу — можно держать десятки тысяч одновременных запросов.
Хорошо структурирует I/O‑bound код (сетевые сервисы, чаты, прокси и т.д.).
Минусы:
Нужно «переписать мозг» под
async‑стиль:awaitтянется по стеку вызовов.Любой блокирующий вызов внутри
async‑кода тормозит весь event loop.Не даёт параллелизма для CPU‑bound (всё равно один поток).
Когда что выбирать?
Кандидату уровня middle обычно нужно уметь уверенно сказать:
I/O‑bound задачи (много ожидания сети/диска, мало CPU):
если библиотека поддерживает async — asyncio (либо фреймворк на нём: FastAPI, aiohttp и т.п.);
если нет async‑API — threading (или смешанный вариант: asyncio + to_thread/run_in_executor).
CPU‑bound задачи (много вычислений):
multiprocessing или внешние воркеры (Celery, RQ и т.п.);
либо выносить тяжёлое в C/NumPy, которые сами умеют использовать потоки.
Если ни I/O‑bound, ни тяжёлый CPU (обычный CRUD‑backend) — можно начать с простого синхронного подхода; async/threads/процессы подключать по мере появления узких мест.
Краткая устная формулировка
В CPython threading и asyncio подходят для I/O‑bound задач: они позволяют перекрывать ожидания I/O, но не дают настоящего параллелизма по CPU из‑за GIL. multiprocessing создаёт отдельные процессы с отдельными интерпретаторами, поэтому подходит для CPU‑bound задач и позволяет использовать все ядра. Asyncio даёт гораздо меньшие накладные расходы и лучше масштабируется по числу задач, но требует писать код в async‑стиле и аккуратно обходиться с блокирующими вызовами.