В CPython есть несколько способов «параллелить» код: потоки (threading) для I/O‑bound задач, процессы (multiprocessing) для CPU‑bound задач, асинхронность (asyncio) для конкурентного I/O в одном потоке и вынос тяжёлых вычислений в нативный код/библиотеки, которые освобождают GIL; ключевое ограничение — GIL, который не даёт по‑настоящему параллельно исполнять байткод нескольких потоков внутри одного процесса.
Как запустить код на Python параллельно? Какие есть ограничения?
Подробный ответ
На уровне senior важно не просто перечислить инструменты, а увязать их с моделью исполнения CPython и типами нагрузок.
Параллелизм через потоки (threading)
Пакет threading даёт абстракции потоков, локального для потока хранилища, примитивов синхронизации и т.п. ОС может распараллеливать потоки по ядрам, но в CPython есть GIL, из‑за которого в каждый момент времени только один поток исполняет Python‑байткод.
Практически:
для I/O‑bound задач (много ожидания сети/диска, мало CPU) потоки работают хорошо: при блокирующем I/O GIL отпускается, и другие потоки могут выполняться;
для CPU‑bound чисто на Python многопоточность почти не даёт выигрыша и может даже ухудшать ситуацию за счёт переключений и конкуренции за GIL.
Параллелизм через процессы (multiprocessing)
multiprocessing создаёт отдельные процессы, у каждого свой интерпретатор и свой GIL. Это позволяет реально задействовать несколько ядер для CPU‑bound задач.
Плюсы:
настоящий параллелизм по CPU, хорош для тяжёлых вычислений;
изоляция по памяти между процессами (повышенная надёжность, отсутствие shared‑state).
Минусы:
дороже создание процессов, тяжёлое IPC и сериализация (
pickle), особенно на Windows;сложнее отладка и управление жизненным циклом воркеров.
Частая практика: CPU‑тяжёлое выносится в пул процессов (локальный через multiprocessing.Pool или внешние воркеры: Celery, RQ и т.п.).
Конкурентность через asyncio
asyncio и фреймворки поверх него (FastAPI, aiohttp, Starlette) дают конкурентное выполнение большого числа I/O‑операций в одном потоке.
всё по‑прежнему крутится в одном потоке, под тем же GIL;
параллелизма по CPU нет, но можно одновременно обслуживать тысячи соединений, пока корутины ждут сеть/диск;
любой длительный CPU‑bound участок внутри event loop блокирует все задачи, его нужно выносить в
run_in_executorили процессы.
Нативные расширения и библиотеки, которые освобождают GIL
Многие вычислительные библиотеки (NumPy, часть SciPy, некоторые ML/crypto‑библиотеки) реализованы на C/C++ и временно отпускают GIL во время тяжёлых участков. Тогда несколько потоков одного процесса могут действительно загружать несколько ядер, хотя байткод Python по‑прежнему защищён GIL.
Ещё один путь — писать свои C‑расширения или использовать Cython и напрямую управлять тем, где отпускать GIL.
Ключевые ограничения и архитектурные выводы
GIL делает многопоточность в CPython плохим выбором для CPU‑bound workloads — для них лучше процессы/нативный код;
для I/O‑bound: выбираем между
threadingиasyncio/ASGI‑фреймворками, в зависимости от экосистемы и удобства разработки;важно понимать, где именно у тебя «параллельность» (процессы, потоки, корутины, нативные библиотеки) и как это влияет на модель ошибок, отладку и деплой.
Формулировка уровня senior
В CPython я могу распараллеливать I/O‑bound задачи потоками или async/await, а CPU‑bound — через процессы или нативный код, который отпускает GIL. При выборе подхода я учитываю накладные расходы на IPC, модель отказов, интеграцию с используемым фреймворком и то, насколько прозрачно это будет поддерживаться командой.
Оцени свой прогресс