Как запустить код на Python параллельно? Какие есть ограничения?

В CPython есть несколько способов «параллелить» код: потоки (threading) для I/O‑bound задач, процессы (multiprocessing) для CPU‑bound задач, асинхронность (asyncio) для конкурентного I/O в одном потоке и вынос тяжёлых вычислений в нативный код/библиотеки, которые освобождают GIL; ключевое ограничение — GIL, который не даёт по‑настоящему параллельно исполнять байткод нескольких потоков внутри одного процесса.

Подробный ответ

На уровне 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, модель отказов, интеграцию с используемым фреймворком и то, насколько прозрачно это будет поддерживаться командой.

Оцени свой прогресс

Честно оцени своё понимание этого вопроса, чтобы мы могли построить твой учебный трек максимально эффективно.
Читать в блоге