При работе с 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
Пример реализации кода с модулем 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 минут.

Код с модулем для окружающей среды Ubuntu 22.04 LTS (8-ядерный CPU), Python 3.10
Код с модулем для окружающей среды Ubuntu 22.04 LTS (8-ядерный CPU), Python 3.10

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

В Windows был аналогичный CPU
В Windows был аналогичный CPU

В качестве эксперимента мы запустили этот код на 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
Пример кода с использованием asyncio

Саммари: вам подойдет 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 задач

Заключение

Что имеем в итоге? Как и везде, нет универсального модуля или метода обработки задач. Выбирайте программное решение исходя из того, что именно требуется автоматизировать или ускорить. Причем на практике часто применяются комбинированные подходы. Например, в асинхронный ввод-вывод встраивается пул процессов для вычислений. Надеюсь, статья поможет вам повысить эффективность своей работы и принесет пользу. Благодарю за прочтение и оставляйте комментарии.

Комментарии (17)


  1. dyadyaSerezha
    01.02.2026 12:44

    1) Я думал, что писать вычислительный код, как в примере, на самом Питоне (без библиотек) - это моветон. Питон же медленный. Прав ли я?

    2) Есть ли библиотеки (Polars?), которые внутри себя организуют параллельные вычисления, не прибегая к каким-либо механизмам самого Питона? Если есть, почему ничего о них не написано?

    3) Есть ли сравнения производительности (по максимальному rps, напрмер) для типичного web-сервиса на Python с Java или C#, где надо обратиться к REST, к БД, что-то совсем простенькое посчитать и записать или отдать результат в БД или по REST? Я к тому, что верно ли утверждение, что писать что-то для high load на Питоне - это плохая идея?


    1. Hell_Grabowsky Автор
      01.02.2026 12:44

      Ответила вам. Коммент ниже


  1. Hell_Grabowsky Автор
    01.02.2026 12:44

    Приветствую.

    1. Для тяжёлых вычислений Python довольно медленный и ограничен GIL. Для простых расчётов, I/O-задач, бизнес-логики и «склейки» компонентов он используется повсеместно. И на практике Python редко считает сам — тяжёлую работу за него делают библиотеки на C или Rust.

    2. Да, конечно есть. NumPy, Polars, PyTorch и другие считают в нативном коде, параллельно и без GIL. Python там обеспечивает только интерфейс. В статье это не подчеркнуто, потому что для экосистемы Python это уже стандартная модель.

    3. Здесь распишу подробнее. По rps:

    ·        Java / C# в среднем дают больше RPS на одном процессе

    ·        Python даёт меньше RPS на процесс, но разница обычно не в разы, а в процентах

    Потому что в таком сервисе:

    ·        70–90% времени уходит на ожидание сети и БД

    ·        Python в это время просто ждёт, а не «медленно считает»

    Например, запрос в БД — 5–20 мс, HTTP-вызов — 10–50 мс, Python-логика — микросекунды. То есть, даже если Python в 2 раза медленнее Java на вычислениях —
    на фоне сетевых задержек это почти незаметно.

    Python используют под high load и весьма успешно. Так как асинхронные серверы (FastAPI, aiohttp) держат десятки тысяч соединений, сервисы масштабируют горизонтально (добавляют инстансы), а тяжёлые вычисления выносятся в отдельные воркеры или нативные библиотеки. В целом, high load решается архитектурой, а не языком.

    Но, если у вас в каждом запросе много CPU-вычислений, нельзя масштабироваться горизонтально и нужно максимум RPS с одного процесса любой ценой, то лучше выбрать Java, C# или Go.


    1. alex88django_novice
      01.02.2026 12:44

      Какую нагрузку ваш сервис на Python будет держать, зависит как минимум от реализации сервера: одно и то же FastAPI приложение можно "поднять" на uvicorn (дефолт) или granian (написан на rust) - rps и latency будут сильно отличаться.

      Далее, если Вы используйте классическую связку FastAPI + Pydantic, то будьте готовы, что Вам придется "платить" за сериализацию и валидацию (да, Pydantic был переписан на rust в свое время, но ему это не особо помогло). Как альтернатива пайдентику - msgspec (написан на С, очень быстрый)

      И так, по тихой грусти, можно придти к тому, что библиотеки на других ЯП под Python - это не только про математику и тяжеловесные CPU-bound вычисления (numpy, polars и т.д.) - это вообще про все, что "вокруг" среднестатистического Python веб-сервиса.
      И тогда логичный вопрос: а что у нас от самого пайтона то осталось, кроме синтаксических конструкций?


      1. Hell_Grabowsky Автор
        01.02.2026 12:44

        Вы абсолютно правы: современный высокопроизводительный Python всё больше напоминает "клей" для эффективных библиотек, написанных на C, Rust или C++. Использование Granian вместо Uvicorn или msgspec вместо Pydantic — это отличные примеры того, как разработчики стараются обойти врожденную медлительность интерпретатора.

        Что же остается от самого Python? Пожалуй, самое ценное — скорость разработки и экосистема. Python стал универсальным интерфейсом: он позволяет строить сложную логику на простом и читаемом языке, делегируя "черную работу" низкоуровневым движкам. В итоге мы получаем лучшее из двух миров: комфорт написания кода и производительность, близкую к нативной. Да, от чистого Python остаются в основном "синтаксические конструкции", но именно они делают разработку доступной и быстрой


        1. alex88django_novice
          01.02.2026 12:44

          В нашей команде давеча написали веб-сервис на пайтоне, а потом еще месяца 2-3 его оптимизировали: затащили polars вместо pandas, переписали «драйверы» с использованием cython, перешли с uvicorn на granian, а после и вовсе - с http на gRPC, добавили dramatiq, чтобы эффективно утилизировать все ядра cpu на подах… А сейчас этот сервис в стадии активного переписывания на go :D


  1. CyrK
    01.02.2026 12:44

    А что лучше для планировщика, который бы гарантированно "тикал" каждую минуту, и при совпадении времени выполнял воспроизведение аудио файла? Т. Е. Как сделать, чтобы тик был бы точно каждую минуту ноль секунд вне зависимости от продолжительности воспроизведения и редактирования планировщика.


    1. Hell_Grabowsky Автор
      01.02.2026 12:44

      Вам подойдет asyncio. Он позволяет реализовать «кооперативную многозадачность»: вычислять точное время до следующего тика, пока звук проигрывается в фоне, не блокируя основной цикл.


      1. CyrK
        01.02.2026 12:44

        Спасибо, к сожалению не имею возможность поставить + из-за кармы.


      1. alex88django_novice
        01.02.2026 12:44

        Для точных кроновых задач («точно каждую минуту 0 секунд») asyncio плохо подходит, да и Python в целом


        1. Hell_Grabowsky Автор
          01.02.2026 12:44

          Согласна, Python — это не система реального времени. Если процессор «ляжет», опоздает любой скрипт. Но для аудио это не критично. Если нужен именно Python, я бы выбрала asyncio. Он не «плывет», так как мы считаем время до :00 на каждом шаге, а не просто ждет по 60 секунд. Так, например:

          while True:

          now = datetime.datetime.now()

          wait = 60 - now.second - now.microsecond/1e6

          await asyncio.sleep(wait)

          asyncio.create_task(play_audio())



          1. alex88django_novice
            01.02.2026 12:44

            в приведенном Вами примере кода нигде не гарантируется, что play_audio будет выполняться строго по таймеру:

            • asyncio.create_task создает задачу и помещает ее в ready_to_run очередь event loop'а, но не запускает ее непосредственно (на самой 1-й итерации sleep будет "холостой")

            • так как у нас (в целом) 1 поток + кооперативная многозадачность, play_audio (по каким либо причинам) может банально не отдавать управление назад в event loop какое-то длительное время (скажем, wait*2), и фьюча, порожденная вызовом asyncio.sleep, в этом случае не будет опрошена ивент-лупом «вовремя» (через wait секунд от момента добавления ее в очередь)

            Тут банально нет какого-то механизма прерывания (как в выталкивающей модели многозадачности), и это, в глобальном смысле, проблема асинхронной модели в Python: кооперативная многозадачность на 1-м потоке.
            И когда при помощи Python + asyncio нужно реализовать что-то чуть более сложное, чем сделать N запросов по сети конкурентно - начинаются танцы с бубнами, придумывание воркэраундов и т.д.


            1. Hell_Grabowsky Автор
              01.02.2026 12:44

              Если asyncio не подходит из-за рисков блокировки событийного цикла, можно же попробовать multiprocessing. Вынос воспроизведения в отдельный независимый процесс гарантирует, что планировщик не будет зависеть от длительности или ошибок выполнения задачи. Достаточно реализовать цикл, который динамически рассчитывает время до начала следующей минуты и запускает новый процесс ровно в «ноль секунд».


              1. alex88django_novice
                01.02.2026 12:44

                А multiprocessing - это overhead на переключение процессов...

                В общем, это уже немного в сторону, но хотелось бы видеть (в обозримом будущем) какую-то более совершенную реализацию асинхронной модели в Python, тем более что в 3.14 GIL выпилили, и это открывает возможность для условного "asyncio V2": с M:N моделью, планировщиком, который будет скедъюдить тысячи async задач на ограниченном (количеством ядер CPU) множестве OS потоков. Эх, прекрасное далёко


                1. OtakSlim
                  01.02.2026 12:44

                  asyncio.to_thread не заблокирует событийный цикл. Выкинули в отдельный поток воспроизведение и дальше считаем время. Нет оверхеда на создание процесса


                  1. alex88django_novice
                    01.02.2026 12:44

                    Угу, а оверхэда на context-switch и борьбу за GIL тоже нет?


                  1. alex88django_novice
                    01.02.2026 12:44

                    а вообще, посмотрите на код, который привела автор(ка) статьи выше.
                    to_thread с create_task в принципе не "дружит", а c run_coroutine_threadsafe результат будет такой же, как и без него.

                    В любом случае, длительность выполнения play_audio влияет на реальное время выполнения sleep -> на время 1 итерации while цикла, т.е. задача, поставленная корневом комментарии - "Как сделать, чтобы тик был бы точно каждую минуту ноль секунд вне зависимости от продолжительности воспроизведения и редактирования планировщика" - не решена