Комментарии 17
1) Я думал, что писать вычислительный код, как в примере, на самом Питоне (без библиотек) - это моветон. Питон же медленный. Прав ли я?
2) Есть ли библиотеки (Polars?), которые внутри себя организуют параллельные вычисления, не прибегая к каким-либо механизмам самого Питона? Если есть, почему ничего о них не написано?
3) Есть ли сравнения производительности (по максимальному rps, напрмер) для типичного web-сервиса на Python с Java или C#, где надо обратиться к REST, к БД, что-то совсем простенькое посчитать и записать или отдать результат в БД или по REST? Я к тому, что верно ли утверждение, что писать что-то для high load на Питоне - это плохая идея?
Приветствую.
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.
Какую нагрузку ваш сервис на Python будет держать, зависит как минимум от реализации сервера: одно и то же FastAPI приложение можно "поднять" на uvicorn (дефолт) или granian (написан на rust) - rps и latency будут сильно отличаться.
Далее, если Вы используйте классическую связку FastAPI + Pydantic, то будьте готовы, что Вам придется "платить" за сериализацию и валидацию (да, Pydantic был переписан на rust в свое время, но ему это не особо помогло). Как альтернатива пайдентику - msgspec (написан на С, очень быстрый)
И так, по тихой грусти, можно придти к тому, что библиотеки на других ЯП под Python - это не только про математику и тяжеловесные CPU-bound вычисления (numpy, polars и т.д.) - это вообще про все, что "вокруг" среднестатистического Python веб-сервиса.
И тогда логичный вопрос: а что у нас от самого пайтона то осталось, кроме синтаксических конструкций?
Вы абсолютно правы: современный высокопроизводительный Python всё больше напоминает "клей" для эффективных библиотек, написанных на C, Rust или C++. Использование Granian вместо Uvicorn или msgspec вместо Pydantic — это отличные примеры того, как разработчики стараются обойти врожденную медлительность интерпретатора.
Что же остается от самого Python? Пожалуй, самое ценное — скорость разработки и экосистема. Python стал универсальным интерфейсом: он позволяет строить сложную логику на простом и читаемом языке, делегируя "черную работу" низкоуровневым движкам. В итоге мы получаем лучшее из двух миров: комфорт написания кода и производительность, близкую к нативной. Да, от чистого Python остаются в основном "синтаксические конструкции", но именно они делают разработку доступной и быстрой
В нашей команде давеча написали веб-сервис на пайтоне, а потом еще месяца 2-3 его оптимизировали: затащили polars вместо pandas, переписали «драйверы» с использованием cython, перешли с uvicorn на granian, а после и вовсе - с http на gRPC, добавили dramatiq, чтобы эффективно утилизировать все ядра cpu на подах… А сейчас этот сервис в стадии активного переписывания на go :D
А что лучше для планировщика, который бы гарантированно "тикал" каждую минуту, и при совпадении времени выполнял воспроизведение аудио файла? Т. Е. Как сделать, чтобы тик был бы точно каждую минуту ноль секунд вне зависимости от продолжительности воспроизведения и редактирования планировщика.
Вам подойдет asyncio. Он позволяет реализовать «кооперативную многозадачность»: вычислять точное время до следующего тика, пока звук проигрывается в фоне, не блокируя основной цикл.
Спасибо, к сожалению не имею возможность поставить + из-за кармы.
Для точных кроновых задач («точно каждую минуту 0 секунд») asyncio плохо подходит, да и Python в целом
Согласна, 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())
в приведенном Вами примере кода нигде не гарантируется, что 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 запросов по сети конкурентно - начинаются танцы с бубнами, придумывание воркэраундов и т.д.
Если asyncio не подходит из-за рисков блокировки событийного цикла, можно же попробовать multiprocessing. Вынос воспроизведения в отдельный независимый процесс гарантирует, что планировщик не будет зависеть от длительности или ошибок выполнения задачи. Достаточно реализовать цикл, который динамически рассчитывает время до начала следующей минуты и запускает новый процесс ровно в «ноль секунд».
А multiprocessing - это overhead на переключение процессов...
В общем, это уже немного в сторону, но хотелось бы видеть (в обозримом будущем) какую-то более совершенную реализацию асинхронной модели в Python, тем более что в 3.14 GIL выпилили, и это открывает возможность для условного "asyncio V2": с M:N моделью, планировщиком, который будет скедъюдить тысячи async задач на ограниченном (количеством ядер CPU) множестве OS потоков. Эх, прекрасное далёко
asyncio.to_thread не заблокирует событийный цикл. Выкинули в отдельный поток воспроизведение и дальше считаем время. Нет оверхеда на создание процесса
Угу, а оверхэда на context-switch и борьбу за GIL тоже нет?
а вообще, посмотрите на код, который привела автор(ка) статьи выше.to_thread с create_task в принципе не "дружит", а c run_coroutine_threadsafe результат будет такой же, как и без него.
В любом случае, длительность выполнения play_audio влияет на реальное время выполнения sleep -> на время 1 итерации while цикла, т.е. задача, поставленная корневом комментарии - "Как сделать, чтобы тик был бы точно каждую минуту ноль секунд вне зависимости от продолжительности воспроизведения и редактирования планировщика" - не решена

Разбор threading vs multiprocessing vs asyncio в Python