Конкурентность, параллелизм, асинхронность в Python

Допустим, наша программа должна скачать 100 страниц с разных сайтов. Если делать по очереди — каждый запрос ждёт ответа сервера 1-2 секунды, и общее время это сумма всех ожиданий. Большую часть этого времени процессор простаивает. Решение очевидно: пока один запрос ждёт ответа, можно отправлять другие. Это и есть конкурентное выполнение.

В Python три инструмента для этого: threading, multiprocessing, asyncio. Эта статья — карта: какой и когда брать.

Три понятия

В разговорах о конкурентности постоянно мешают три похожих слова. Различие важное:

  • Конкурентность (concurrency): задачи могут переключаться между собой, создавая иллюзию одновременной работы. Один бариста за стойкой принимает заказ, ставит молоко греться, переходит к следующему клиенту, возвращается к молоку. Один исполнитель, несколько задач «в воздухе» одновременно.

  • Параллелизм (parallelism): задачи выполняются физически одновременно на разных ядрах процессора. Несколько бариста, каждый делает свой кофе. Требует многоядерности.

  • Асинхронность (asynchronicity): способ организации кода, при котором задача может «отложиться» в ожидании (например, ответа сервера), не блокируя весь поток. Это способ достижения конкурентности на одном потоке, без переключения ОС.

Конкурентность это цель, параллелизм и асинхронность — два способа её достичь.

Иллюстрация: три временные шкалы — параллелизм (две задачи одновременно на двух ядрах), конкурентность (одна шкала с чередующимися задачами), асинхронность (одна шкала, где паузы await заполняются другими задачами)

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 в legacy-коде без async-библиотекthreading
Тяжёлые вычисления на нескольких ядрахmultiprocessing
Простое распараллеливание без погружения в деталиconcurrent.futures (ThreadPoolExecutor / ProcessPoolExecutor)

В дальнейших статьях разберём каждый по очереди. Сначала потоки и процессы, потом asyncio (две статьи: основы и продвинутые техники).

Проверка понимания

Какое утверждение наиболее точно описывает разницу между параллелизмом и асинхронностью?


В следующей статье разберём потоки и процессы — два классических подхода. Поток это легковесный исполнитель внутри одного процесса (для I/O), процесс это отдельная программа с собственной памятью (для CPU-bound).