При работе с Python да и другими языками программирования часто возникает необходимость ускорения выполнения кода, масштабирования обработки данных или работы с большим количеством сетевых запросов. Именно в Python для решения этих задач существуют три базовых метода. Это: threading, multiprocessing и asyncio. На первый взгляд – механизмы схожие. Но при детальном разборе ясно, что они решают принципиально разные задачи, опираются на разные модели исполнения и обладают своими ограничениями. В статье расскажу об особенностях каждого метода – будет интересно и познавательно.
Конкурентность и параллелизм в Python
Перед тем, как рассказать о конкретных инструментах Python, стоит выделить и определить два базовых понятия �� конкурентность и параллелизм.
Конкурентность (concurrency) – это способность ПО логически выполнять несколько задач одновременно. Однако эти задачи не всегда исполняются параллельно на уровне процессора. Если кратко, конкурентность – способ организации работы, при котором задачи быстро переключаются между друг другом, что создает ощущение одновременной работы.
Параллелизм (parallelism) – это фактическое одновременное выполнение нескольких вычислений на разных ядрах процессора. Здесь работа невозможна без соответствующей аппаратной поддержки и в Python напрямую упирается в архитектуру интерпретатора.
Для Python принципиально важно наличие Global Interpreter Lock (GIL) — глобальной блокировки интерпретатора. Благодаря ей в каждый момент времени байткод Python исполняется только одним потоком. Подобный подход упрощает управление памятью, но накладывает серьёзные ограничения на использование потоков для задач, значительно нагружающих CPU. В конечном итоге именно наличие/отсутствие GIL определяет различия в поведении модулей threading, multiprocessing и asyncio.
Модуль threading: обеспечиваем многопоточность в границах одного процесса
Threading – стандартный интерфейс, который используется для создания и управления потоками выполнения внутри одного процесса. В Python каждый поток представлен объектом Thread.
Поток запускается методом start(), далее интерпретатор передаёт управление целевой функции.
Метод join() нужен для ожидания завершения потока, что в итоге позволяет синхронизировать выполнение.
Основным ограничением threading является GIL. Да, потоки могут переключаться часто, но, как и сказано выше, в каждый момент времени только один из них исполняет байткод. По этой причине многопоточность не улучшает производительность для CPU-bound задач. В том числе метод не подойдет для обработки изображений, математических вычислений или сжатия данных.
При этом threading просто необходим для обработки I/O-bound задач, где основное время тратится на ожидание ввода-вывода. Во время блокирующих операций (сетевые запросы или чтение файлов) GIL освобождается, а другие потоки могут свободно работать. Поэтому чаще всего модуль применим для сетевых клиентов, загрузчиков данных и API с внешними сервисами.
Типичные проблемы и синхронизация
Потоки разделяют общую память, по этой причине разработчику нужно самостоятельно обеспечить к��рректный доступ к разделяемым данным. Для выполнения этой задачи в threading есть примитивы синхронизации. В их число входят: Lock, RLock, Semaphore и Condition.
Lock — запрещает нескольким потокам одновременно заходить в критическую секцию.
RLock (reentrant lock) — позволяет одному и тому же потоку захватывать его многократно без блокировки самого себя.
Semaphore — счётчик разрешений, ограничивает число потоков, способных одновременно выполнять конкретную операцию.
Condition — условная переменная, при ее использовании потоки ждут определённое событие и способны сообщать друг другу о моменте, когда оно наступит.
Обратите внимание. Некорректное применение блокировок приводит к двум классическим проблемам многопоточного программирования — race condition и deadlock. В первом случае результат работы программы становится непредсказуемым, во втором — потоки навсегда блокируют друг друга.
Кейс и пример кода
Была задача – скачать и сохранить на локальный SSD 150 файлов с сервера. При последовательной загрузке было бы потрачено много времени на ожидание ответа сервера. При использовании threading каждая загрузка запускалась в отдельном потоке. Пока часть потоков ждала, другие уже работали.

Саммари: threading нужен, когда программа часто ждёт внешние ресурсы (сеть, файлы, API) и важно не простаивать. Не нужен для ускорения тяжёлых вычислений.
Модуль multiprocessing: убираем ограничения GIL
В отличие от threading модуль multiprocessing создаёт в ОС отдельный процесс для выполнения каждой задачи. У каждого отдельного процесса есть собственное адресное пространство и выделенный экземпляр интерпретатора Python. Также каждому процессу назначается свой GIL. Это отличие принципиально. Благодаря ему модуль multiprocessing стал основным в Python для распараллеливания CPU-bound задач.
Базовая единица работы здесь – объект Process. Его интерфейс схож с threading.Thread, что часто вводит в заблуждение junior-специалистов.
Для запуска процесса используется метод start().
Завершение отслеживается через join().
Несмотря на схожесть кода в данном модуле применима иная модель исполне��ия с другими требованиями к архитектуре, чем в threading. Главный плюс multiprocessing – он способен задействовать несколько ядер процессора для параллельных вычислений. Это позволяет задействовать модуль для:
задач численного анализа;
обработки изображений;
машинного обучения;
криптографических операций.
В целом, он подходит для любых сценариев, где вычислений значительно больше, чем операций ввода-вывода данных.
Межпроцессное взаимодействие
Из-за изоляции процессов они не разделяют память напрямую. Чтобы обеспечить обмен данными, здесь используются отдельные очереди и каналы (Queue, Pipe). Также применимы структуры совместной памяти, например, Value и Array. В новых версиях Python (3.8 и выше) стала доступна полноценная shared memory. Что делают основные механизмы модуля:
Queue — очередь для передачи данных между процессами. Основана на сериализации объектов через pickle. При передаче больших объёмов данных часто снижает производительность.
Pipe — обоюдный канал связи между двумя процессами. Работает быстрее, чем Queue, но также использует сериализацию и применим только для ограниченного числа участников.
Value и Array — примитивы совместной памяти. Служат для хранения простых типов данных. С ними не нужна сериализация, но они ограничены по структуре и требуют аккуратной синхронизации доступа.
Shared memory (Python 3.8+) — техника совместного использования памяти без копирования данных. Нужна для работы с объёмными БД, но требует ручного управления жизненным циклом памяти.
Также можно использовать прокси-объект Manager. Его задача – обеспечивать работу с общим состоянием (словари, списки и другие структуры) для сразу нескольких процессов. Объект удобен, но отличается высоким overhead из-за постоянного межпроцессного взаимодействия.
Обратите внимание. В зависимости от того, какую ОС вы используете, процессы создаются по-разному. На Unix-подобных системах применима функция вызова ядра ОС «fork», и она даёт возможность относительно быстро клонировать процесс. Для Windows потребуется «spawn». Его минус: процесс не клонируется, вместо этого выполняется перезапуск интерпретатора с нуля. Так как весь модуль стартует заново, здесь обязательно наличие конструкции if name == "__main__".
Кейс и примеры кода
Один из моих знакомых IT-специалистов должен был обработать массив, содержащий 1,2 млн телеметрических записей от промышленного оборудования. Попытка обработки через threading результатов не дала (помешали ограничения GIL), вообще без дополнительных модулей программа обрабатывала информацию 15 минут. Неплохо, но при использовании метода multiprocessing общее время вычислений сократилось до 2 минут.

Если бы он работал на Windows и через «spawn», код был бы таким:

В качестве эксперимента мы запустили этот код на Windows. Если в Ubuntu время вычислений составило 2 минуты, то на ОС Win10 (с тем же 8-ядерным процессором) – 2 минуты 30 секунд.
Саммари: multiprocessing – метод мощный, но ресурсоёмкий. Учитывайте, что все функции должны быть сериализуемыми, а глобальные объекты нужно использовать с осторожностью. Подход хорош для CPU-bound задач с большим объёмом независимых вычислений, но неэффективен для I/O-bound.
Асинхронная модель без потоков и процессов - asyncio
В отличие от предыдущих у модуля asyncio принципиально другой подход к обработке данных. Здесь используется асинхронная кооперативная многозадачность. Имеет место явная передача управления между задачами, а не за счёт вытесняющего планировщика. Принцип работы asyncio:
Создаётся цикл событий (event loop) – служит для управления выполнением всех корутин и задач.
Определяются корутины – функции, объявленные через async def. По факту это «отложенные» задачи, которые можно приостанавливать и запускать снова.
В корутинах выставляются точки приостановки с await – при данном условии выполнение функции приостанавливается на время, управление возвращается циклу событий.
Цикл событий распределяет выполнение – если одна корутина стоит на паузе, цикл способен запускать другие корутины. Благодаря этому обеспечивается быстрое переключение между задачами без необходимости создания дополнительных потоков.
Модель исключает проблемы синхронизации и состояния гонки на уровне памяти. Но есть нюанс – любые операции ввода-вывода должны быть неблокирующими или вынесены в отдельные исполнители (executor). В противном случае они способны остановить выполнение всего цикла.
Преимущества и ограничения asyncio
В процессе взаимодействия с модулем были выявлены следующие плюсы:
один поток способен обслуживать тысячи I/O-операций параллельно без значительных накладных расходов;
минимальное потребление ресурсов системы при высоком числе соединений;
подходит для сетевых приложений (веб-серверы, прокси, API-шлюзы).
Дополнительное преимущество – цикл событий распределяет задачи централизованно, из-за чего упрощается контроль за выполнением I/O. Нашли и минусы:
asyncio не подходит для CPU-bound задач, потому что длительные вычисления блокируют event loop и замедляют выполнение кода;
в модуле все тяжёлые вычисления нужно выносить в multiprocessing или в пул потоков (loop.run_in_executor);
не все сторонние библиотеки поддерживают неблокирующий ввод-вывод (подходят aiohttp, asyncpg, aioredis, aiofiles и ряд других).
Отдельная проблема – сложности в сочетании синхронного и асинхронного кода. При невнимательном проектировании возможны блокировки программы или «зависание» event loop.
Обратите внимание. Отладка асинхронного кода сложнее из-за нелинейного порядка выполнения корутин.
Кейс по асинхронной обработке сетевых запросов
IT-команде нужно было получить данные с 1 500 внешних API, обработать их и сохранить в БД. При синхронных запросах через requests полный рабочий цикл занимал около 45 минут. Интеграция в код модуля asyncio сократила это время буквально до 1,5 минут. Проект реализовали на Win 10, Python 3.10, Intel Core i7-10700K (8 ядер, 16 потоков, частота до 5,1 ГГц).

Саммари: вам подойдет asyncio, если требуется выполнение I/O-bound задач, работа с сетью, файлами или БД. Но модуль неэффективен для CPU-bound задач (долгие вычисления блокируют event loop).
Итоговое сравнение модулей
Параметр / показатель | Threading | Multiprocessing | Asyncio |
Тип параллельности | Логическое переключение задач | Многопроцессорное выполнение | Event loop |
Использование CPU | Ограничено GIL | Полностью использует ядра CPU | Не нагружает CPU |
Использование памяти | Общая память между потоками | У каждого процесса свое пространство | Один поток |
Поддержка I/O-bound задач | Отлично | Хорошо | Отлично |
Поддержка CPU-bound задач | Плохо | Отлично | Плохо |
Накладные расходы на создание задач | Низкие | Высокие | Низкие |
Сложность синхронизации | Высокая | Средняя | Низкая |
Взаимодействие между задачами | Общая память, примитивы синхронизации | Очереди, каналы, shared memory, Manager | Передача через await, coroutines |
Поддержка платформ | Кроссплатформенная | ||
Сложность отладки | Средняя | Средняя | Высокая |
Тип задач | I/O-bound | CPU-bound | I/O-bound |
Примеры использования | API-клиенты, загрузчики файлов, сетевые клиенты | Обработка массивов данных, численный анализ, ML, криптография | HTTP-клиенты/серверы, WebSocket, брокеры сообщений, асинхронные файлы |
Старт задач | Немедленный | Медленный на Windows, быстрый на Unix | Немедленный |
Поддержка сторонних библиотек | Все синхронные | Все синхронные | Асинхронные или через run_in_executor |
Недостатки | Deadlock, race condition | Высокие накладные расходы, сериализация | Блок event loop, сложная архитектура |
Простота написания кода | Средняя | Средняя | Средняя / высокая |
Масштабируемость | Ограничена числом потоков и GIL | Масштабируется по ядрам CPU | Масштабируется по количеству I/O задач |
Заключение
Что имеем в итоге? Как и везде, нет универсального модуля или метода обработки задач. Выбирайте программное решение исходя из того, что именно требуется автоматизировать или ускорить. Причем на практике часто применяются комбинированные подходы. Например, в асинхронный ввод-вывод встраивается пул процессов для вычислений. Надеюсь, статья поможет вам повысить эффективность своей работы и принесет пользу. Благодарю за прочтение и оставляйте комментарии.
