Конкурентность, параллелизм, асинхронность в Python
Допустим, наша программа должна скачать 100 страниц с разных сайтов. Если делать по очереди — каждый запрос ждёт ответа сервера 1-2 секунды, и общее время это сумма всех ожиданий. Большую часть этого времени процессор простаивает. Решение очевидно: пока один запрос ждёт ответа, можно отправлять другие. Это и есть конкурентное выполнение.
В Python три инструмента для этого: threading, multiprocessing, asyncio. Эта статья — карта: какой и когда брать.
Три понятия
В разговорах о конкурентности постоянно мешают три похожих слова. Различие важное:
-
Конкурентность (concurrency): задачи могут переключаться между собой, создавая иллюзию одновременной работы. Один бариста за стойкой принимает заказ, ставит молоко греться, переходит к следующему клиенту, возвращается к молоку. Один исполнитель, несколько задач «в воздухе» одновременно.
-
Параллелизм (parallelism): задачи выполняются физически одновременно на разных ядрах процессора. Несколько бариста, каждый делает свой кофе. Требует многоядерности.
-
Асинхронность (asynchronicity): способ организации кода, при котором задача может «отложиться» в ожидании (например, ответа сервера), не блокируя весь поток. Это способ достижения конкурентности на одном потоке, без переключения ОС.
Конкурентность это цель, параллелизм и асинхронность — два способа её достичь.

I/O-bound vs CPU-bound: ключевая дихотомия
Выбор инструмента зависит только от того, чего ждёт ваша задача:
-
I/O-bound: процессор простаивает в ожидании внешнего ресурса. Сетевой запрос, чтение с диска, ответ БД. Здесь побеждает асинхронность — пока один запрос ждёт, отправляем следующие.
-
CPU-bound: процессор честно работает над вычислениями. Сжатие изображения, шифрование, научные вычисления. Здесь нужен реальный параллелизм на нескольких ядрах.
Самая частая ошибка новичка: брать multiprocessing для скачивания страниц или asyncio для перемножения матриц. Это даст замедление, не ускорение.
GIL: почему threading не помогает с CPU
В стандартной реализации Python (CPython) есть Global Interpreter Lock (GIL) — глобальный замок, который разрешает выполнять Python-код только одному потоку за раз внутри процесса. Даже если у вас 8 ядер, потоки выполняются по очереди.
Что из этого следует:
- Для CPU-bound задач threading бесполезен — потоки делят одно ядро через GIL. Нужны процессы (multiprocessing), у каждого свой GIL и своё ядро.
- Для I/O-bound задач GIL отпускается при ожидании сети/диска. Поэтому threading отлично подходит для I/O, как и asyncio (но без накладных расходов на потоки).
Какой инструмент когда
В дальнейших статьях разберём каждый по очереди. Сначала потоки и процессы, потом asyncio (две статьи: основы и продвинутые техники).
Проверка понимания
Какое утверждение наиболее точно описывает разницу между параллелизмом и асинхронностью?
В следующей статье разберём потоки и процессы — два классических подхода. Поток это легковесный исполнитель внутри одного процесса (для I/O), процесс это отдельная программа с собственной памятью (для CPU-bound).
