Хватит спорить — пора запускать и сравнивать.
Тестируем реальные сценарии, измеряем RPS, смотрим на потребление памяти и разбираемся, когда самая разумная стратегия — это просто подождать и обновить Python на free-threading версию.
Привет, Хабр! Меня зовут Игорь Анохин, я — руководитель платформенной разработки в K2 Cloud и более 8 лет программирую на Python.
В чём проблема
Хочу поговорить про асинхронность и многопоточность как про осознанный выбор, который мы используем в своём проекте. K2 Облако — это первое публичное облако собственной разработки, которое мы строим с 2009 года. За последние несколько лет мы достаточно сильно выросли и активно нанимаем Python-разработчиков разного уровня в офис и на удалёнку. Я часто провожу технические интервью, и с каждым разом всё сильнее замечаю тенденцию: разработчики, особенно начинающие, практически не смотрят в сторону многопоточности. Многие ограничиваются только асинхронным подходом, а если и слышали про потоки, то всерьёз сравнить их с асинхронностью не могут. Сам я начинал в те времена, когда асинхронность ещё не была мейнстримом, и многопоточность казалась логичным путём.
K2 Облако — это большой многопоточный проект, который пишется уже более 10 лет. Асинхронность мы тоже используем: ещё на Python 2.7 у нас был самописный движок, IOLoop. Поэтому я хорошо представляю, как работают асинхронные библиотеки, знаю их плюсы и ограничения. И, что интересно, это знание в итоге только усиливает мою симпатию к многопоточности. Особенно сейчас, когда появилась версия Python 3.13 с поддержкой No GIL — интерпретатора без глобальной блокировки.
Что брать для CPU-bound: Threading или asyncio
Задачи, которые мы программисты на Python, решаем, делятся на два типа: IO-bound и CPU-bound. Если речь идёт о CPU-bound задачах, то выбор очевиден: лучше использовать multiprocessing, потому что ни асинхронность, ни многопоточность в чистом виде не дают желаемой производительности в условиях GIL.
А вот в случае с IO-bound задачами, на мой взгляд, оба подхода — многопоточность и асинхронность — дают схожий результат. Почему? Разберём на примере.
Пример многопоточности
Рассмотрим небольшую функцию, которая отправляет запросы к сайту k2.cloud. У неё сетевое ожидание, поэтому если мы хотим ускорить запуск таких задач в многопоточном подходе, то используем ThreadPoolExecutor.
def fetch(_):
return requests.get("https://k2.cloud")
n_requests = 100
with ThreadPoolExecutor(
max_workers=n_requests
) as executor:
results = list(executor.map(fetch,
range(n_requests)))
Чтобы понять, как на самом деле работает этот код и как создаются потоки — нужно заглянуть в исходники CPython. Внутри используется вызов pthread_create — стандартная функция из C-библиотеки, которая создаёт «честный» поток.
static long
PyThread_start_new_thread(void (*func)(void ), void arg)
{
pthread_t th;
pthread_attr_t attrs;
/* ... attribute initialization ... */
if (pthread_create(&th, &attrs,
(void ()(void *))func, arg) != 0)
return -1;
/* ... cleanup ... */
return (long)th;
}
Функция pthread_create, которую можно найти в сниппете, означает, что каждый поток Python на самом деле соответствует полноценному потоку операционной системы (OS thread). Такой же поток мы получили бы в любом другом языке программирования с «честной» многопоточностью.

Операционная система управляет этими потоками через собственный планировщик (scheduler), который не знает, что внутри выполняется код на Python. Планировщик просто распределяет время процессора между потоками, как и для любого другого процесса, вне зависимости от языка программирования. То есть на уровне операционной системы GIL нам никак не мешает.
Чтобы понять, где именно мы теряем скосроть, давайте разберём, как это работает на уровне Python. Первый поток, которому операционная система даст время выполнения, захватывает GIL (Global Interpreter Lock) и выполняется. Когда операционная система даёт процессорное время следующему потоку, он начинает своё выполнение с попытки захватить GIL. Так как GIL занят, поток вынужден ждать его освобождения, поэтому переходит в режим ожидания с помощью системного вызова pthread_cond_wait. Он приостанавливает поток до тех пор, пока GIL не станет доступен.

pthread_cond_wait приостанавливает выполнение потока до тех пор, пока другой поток не освободит GIL. В результате второй и третий потоки находятся в состоянии ожидания, пока GIL не станет доступен. Операционная система самостоятельно определяет, когда поток может быть возобновлён. Такой механизм позволяет эффективно использовать ресурсы и избежать активного ожидания.
Если изображать выполнение потоков программы схематично, то оно будет выглядеть примерно так:

Когда у нас три потока, из-за GIL выполнять одновременно CPU-операции может только один из них. Пока первый поток выполняет CPU-нагруженную задачу, остальные потоки вынужденно ждут. Однако, когда поток переходит к IO-операции, он отпускает удержание GIL, поэтому, другие потоки могут получить управление и начать параллельную работу. Именно за счёт таких моментов ожидания, связанных с IO, и достигается выигрыш в производительности при многопоточности в Python не за счёт параллельного CPU, а за счёт перекрытия времени ожидания IO другими задачами.
Есть важный нюанс: планировщик операционной системы сам решает, когда выделить потокам время на выполнение. При этом GIL в CPython заставляет поток освобождать его примерно раз в 5 миллисекунд. Из-за этого поток может не успеть дойти до IO-операции, и управление передаётся другому потоку. В результате сначала могут выполниться три CPU-операции в разных потоках, а затем уже начнётся параллельная обработка IO.

Тем не менее, общее время выполнения задачи не меняется — все три потока завершатся примерно одновременно, когда закончится последний из них. Так устроена многопоточность в Python с GIL.
Пример асинхронности
Как обстоят дела в асинхронной модели? Здесь задействован всего один поток, и задача — максимально эффективно использовать его ресурсы. Это означает, что мы стараемся минимизировать простои этого потока, переключаясь между задачами в моменты ожидания ввода-вывода (IO), чтобы не блокировать выполнение программы.
Вот асинхронный код, который делает то же самое, что и в примере многопоточности:
async def fetch(session: aiohttp.ClientSession):
return await session.get("https://k2.cloud")
async with aiohttp.ClientSession() as session:
tasks = [
asyncio.create_task(fetch(session))
for _ in range(100)
]
await asyncio.gather(*tasks)
Асинхронность выполняется через event loop, который управляет задачами (tasks). Цикл событий начинает выполнение задачи и продолжает её выполнение до первого await. Встретив первый await, event loop углубляется во вложенные await вызовы до тех пор, пока не встретит операцию, действительно требующую ожидания — например, сетевой запрос или чтение файла.

Встретив ожидание, asyncio регистрирует соответствующий Socket в операционной системе, в данном случае Linux. Этот Socket используется как механизм оповещения: операционная система запишет туда результат работы.
После того, как все созданные Socket’ы зарегистрированы, event-loop опросит их через EPoll. Если другие задачи уже готовы к выполнению, например, задача 2 или 3, epoll вызывается с нулевым таймаутом. Это позволяет сразу получить готовые события без блокировки, не дожидаясь остальных. Если готовых к исполнению задач нет, то event loop рассчитывает подходящий таймаут для сна и вызывает epoll с этим значением, чтобы не нагружать процессор впустую. Как только одно или несколько событий становятся доступны, обработка продолжится, и будет выполняться следующая готовая задача.
В рамках схемы конкурентного выполнения задач асинхронность выглядит так:

Сходство и различие асинхронности и многопоточности
Правда, схема очень напоминает предыдущую, с многопоточностью? Если текущая задача выполняет CPU-операцию, то остальные задачи приостанавливаются и ожидают своей очереди. Они смогут продолжить выполнение только тогда, когда текущая задача передаст управление — например, при переходе к операции ввода-вывода (IO).

В сценарии со множеством задач, где основное время уходит на ожидание IO, асинхронный и многопоточный подходы в Python по сути приходят к схожему финальному времени выполнения. Общая продолжительность выполнения всех задач оказывается сопоставимой. В обоих случаях реальная выгода достигается за счёт немедленного переключения на другую задачу в моменты ожидания, а не параллельного исполнения CPU-операций.
Что же общего и разного у этих подходов? Давайте разбираться.
-
Сходство в ускорении
И в многопоточном, и в асинхронном подходе ускорение достигается главным образом за счёт IO операций. Именно во время их ожидания появляются возможности для параллельной или чередующейся обработки других задач.
-
Важное отличие в переключении задач
В многопоточности переключением задач занимается операционная система, тот самый Scheduler. Планировщик самостоятельно распределяет выполнение между потоками, переключает thread’с между собой. В асинхронности переключение задач контролируется библиотекой asyncio через await. Если не поставить await перед операцией, то можно заблокировать весь event loop, например, тяжёлыми операциями.
-
Сложность — несопоставима
Асинхронный код сложнее для восприятия и изучения. Ошибки в нём могут обойтись дороже из-за их критичности и неочевидности. Ведь долгие годы экспертиза разработчиков формировалась именно в многопоточном стиле программирования, и чтобы переписать многопоточный проект на асинхронный манер, нужно чуть ли не нанимать новую команду. Дебажить такой проект и понимать, как он работает внутри — сложнее. Сейчас поясню, почему.
Когда асинхронность оказывается многопоточностью
Вот пример асинхронной программы, которую сгенерировал Chat GPT для написания файлов. Кажется, что это асинхронность, ведь в ней есть async и await:
import aiofiles
async def write_file(i):
filename = os.path.join(dir_path, f"file_{i}.txt")
async with aiofiles.open(filename, "w") as f:
await f.write("Hello, world!\n" * 10)
Но есть нюанс: если открыть реализацию, то окажется, что внутри асинхронных вызовов используется “await loop.run_in_executor()”, то есть, операция записи не считается по-настоящему асинхронной. Внутри запускается новый поток:
async def write(self, s):
...
cb = partial(self._file.write, s)
return await self._loop.run_in_executor(self._executor, cb)
Если сравнить этот пример со схемами из примеров выше, то выполнение будет выглядеть так:

CPU-нагрузка может быть и внутри async-кода, но большинство операций, таких как работа с файлами через aiofiles, на самом деле выполняются в thread pool. Это означает, что прироста в производительности относительно многопоточности не будет. Архитектура усложняется, чтобы сохранить совместимость с асинхронным интерфейсом. И всё ради того, чтобы асинхронное приложение могло использовать библиотеки, которые по сути остаются синхронными.
Можно подумать, что это нерелевантно для микросервисов или работы с БД. Но похожее происходит и в больших библиотеках. Например, Motor — асинхронный драйвер для MongoDB. Это серьёзный проект, и в его документации указано, что он способен обрабатывать десятки тысяч запросов в секунду. Но проблема в том, что внутри Motor используется PyMongo, старый добрый синхронный драйвер для БД. По сути, Motor внутри себя создаёт несколько экземпляров PyMongo и оборачивает их в thread pool, предлагаю асинхронный интерфейс. То есть под капотом это всё та же многопоточность.
А что насчёт более крупных проектов? Например, Django — классический синхронный фреймворк, который в какой-то момент начал двигаться в сторону асинхронности. В нём добавили возможность писать асинхронные view-функци, middleware и прочие обработчики.
При этом асинхронные middleware в Django выглядят довольно громоздко. Чтобы их реализовать, нужно использовать специальный декоратор, а внутри описать сразу две функции. Одна нужна для синхронного формата, другая — для асинхронного. Можете сами посмотреть, как это выглядит:
Код
def sync_view(request):
http_call_sync()
return HttpResponse("Blocking HTTP request")
async def async_view(request):
loop = asyncio.get_event_loop()
loop.create_task(http_call_async())
return HttpResponse("Non-blocking HTTP request")
Пользователи написали асинхронные MiddleWare:
def simple_middleware(get_response):
# One-time configuration and initialization goes here.
if iscoroutinefunction(get_response):
async def middleware(request): ...
else:
def middleware(request): …
return middlewar
При этом в Django всё ещё используется множество middleware, которые работают только в синхронном режиме. Из-за этого Django вынужден внутри себя переключаться между асинхронным и синхронным контекстом. Эти переключения занимают время и добавляют накладные расходы, замедляя выполнение кода.
Значит ли это, что асинхронность переоценена? Зачем вообще большие библиотеки на неё переходят? Для примера можно посмотреть на PsycoPG 3, которая фактически задублировала кодовую базу и реализовала аналогичные методы и классы, но уже с префиксом async в названиях. Это очень похоже на то, как выглядит Django. Однако PsycoPG 3 при этом получила вот такие показатели по скорости:

В синхронной третьей версии по замерам мы получали около 700-800 RPS. А в асинхронной — 2200-2500, то есть в 3-4 раза больше.
Сравнение скорости работы
Как и почему происходит такой рост? Разберём на примере:
# THREADING
def sync_task(_):
return time.sleep(0.1)
with ThreadPoolExecutor(max_workers=n_requests) as executor:
results = list(executor.map(fetch, range(n_tasks)))
# Async
async def async_task():
return await asyncio.sleep(0.1)
async def run():
tasks = [asyncio.create_task(async_task()) for in range(ntasks)]
return await asyncio.gather(*tasks)
В верхней части примера — синхронный код, реализующий ожидание и выполнение операций последовательно. В нижней части — асинхронный код, в котором каждая операция оформляется как отдельная задача (task) и управляется через event loop.
В синхронной версии на каждый «псевдо-запрос» создаётся поток (thread). В асинхронной версии на каждый запрос создаётся задача (task), которая которая на время ожидания await будет передавать управления event loop.
Теперь запустим оба варианта на разных объёмах входных данных — чтобы сравнить производительность и поведение в зависимости от количества запросов.
Число запросов |
Threading |
Async |
100 |
1.01 |
1.01 |
1000 |
1.07 |
1.01 |
Видно, что при количестве запросов до 1000 разницы в скорости практически нет. От запуска к запуску она будет лишь в пределах допустимой погрешности. Разница становится заметной начиная с 1000 запросов и больше, но и она не очень велика.
Но что будет, если увеличить число запросов ещё в 10 раз, до 10 тысяч? В случае асинхронности код справится за 1.05, а в случае многопоточности — будет ошибка.
Число запросов |
Threading |
Async |
100 |
1.01 |
1.01 |
1000 |
1.07 |
1.01 |
10000 |
RuntimeError: can’t start new thread |
1.06 |
Причина в том, что нельзя создавать такое количество threads в многопоточности. Проблема совсем не в скорости, а в объёме и в количестве затрачиваемых ресурсов. В многопоточном подходе мы вынуждены ограничивать количество создаваемых потоков — это ограничения операционных систем. Поэтому для обработки 10 тысяч запросов уже не получится создать такое же количество потоков.
В случае асинхронности в тот самый единственный работающий поток просто добавляется новый сокет на операционную систему, не превышая её лимиты. Вот как это можно представить:

Способность сокетов к масштабированию выше, чем у многопоточности. Это выражается не только в количественных показателей ограничения потоков на 1 процесс, но и в занятой оперативной памяти. Каждый созданный thread в операционной системе требует 4 МБ. В них нужно хранить стек, информацию, а это дорого. Каждая созданная задача в asyncio занимает всего 4 КБ, ведь в этом случае достаточно создать сокет. Таким образом, тысяча потоков займут 4 ГБ оперативной памяти, в то время как асинхронность займёт порядка 100 МБ. Поэтому даже при примерно одинаковой скорости работы, асинхронность будет менее ресурснозатратна.
На больших данных в скорости тоже будут появляться изменения. Асинхронность будет выигрывать из-за переключения контекста, происходящего, когда поток готов работать. Потому что когда у нас тысяча потоков, переключение контекста в многопоточности начинает занимать ощутимое время. А ещё у нас повысится нагрузка на планировщик задач. Даже если поток спит, планировщик задач видит его в своём списке, и должен его в этом случае игнорировать. Это требует чуть больше логики, которая тоже влияет на производительность. В asyncio работа происходит только с тем, что готово работать.
Кто победил, или пара слов о No GIL
Значит ли это, что Async её победил? И да, и нет.
В примере, который я показывал, нагрузка была полностью направлена на ввод-вывод — IO-bound без учёта реальных особенностей современных веб-приложений. Однако типичное веб-приложение сегодня устроено иначе, особенно популярные решения на Python, где активно используются Pydantic, DTO-классы и сериализация в JSON.
Процесс обработки запроса обычно происходит так: приходит HTTP-запрос -> происходит роутинг и валидация через Pydantic -> формируется DTO -> данные сериализуются в JSON -> выполняется запись или чтение из базы данных через ORM.
Если оценить долю CPU и IO операций, то примерно 20% времени уходит на CPU-операции, а 80% — на IO. С появлением Python сборок без GIL многопоточность может использовать эти 20% CPU-времени значительно эффективнее. Асинхронность же в новых версиях Python никак не начинает эффективнее работать с CPU операциями. Поэтому при смешанных нагрузках подход с многопоточностью может оказаться производительнее. Общая схема по CPU и IO операциям будет выглядеть так:

Давайте возьмем простой пример FastAPI приложения в своей синхронной и асинхронной версии и посмотрим, как оно будет работать:
@app.post("/sync-store")
def sync_store(item: Item):
try:
sync_redis.set(item.key, item.value)
return {"status": "success"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/async-store")
async def async_store(item: Item):
try:
await async_redis.set(item.key, item.value)
return {"status": "success"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
В приведённом выше примере используется упрощённый код одного из наших сервисов, на котором мы тестировали возможности Python 3.13 с No GIL. Первый вариант делает POST-запросы синхронно с использованием внутренней многопоточности FastAPI, второй — асинхронно. Этот код мы запускаем в Python3.13 с No GIL и на обычном Python 3.13.
Многопоточность с GIL даёт около 2800 запросов в секунду. Этот результат можно принять за базовую точку сравнения.
Если использовать асинхронный маршрут, получаем почти 3500 запросов в секунду. Это прирост примерно на 600 запросов по сравнению с многопоточностью.
С No GIL асинхронность остаётся на том же самом уровне. Она чуть быстрее, чем была. С многопоточностью в No GIL получаем 3540. Таким образом, у нас многопоточность в этом варианте становится чуть быстрее асинхронности. Таким образом, просто поменяв версию Python, мы получили прирост скорости, равный переписыванию всего нашего кода с синхронного на асинхронный формат.
Выводы
Для меня асинхронность — это, в первую очередь, не про скорость, а про экономию ресурсов. Если у нас до 1000 одновременных запросов, выбор между многопоточностью и асинхронностью не критичен. Многопоточность займёт, например, 4 ГБ оперативной памяти, и это допустимо. Если же речь идёт о более чем 10 000 одновременных соединений, асинхронность действительно эффективнее. При такой нагрузке мы обычно уже масштабируем приложение по горизонтали. Горизонтальное масштабирование эффективно для асинхронного и многопоточного подхода и будет давать прирост RPS.
Единственное реальное преимущество асинхронности — это снижение стоимости эксплуатации. Она позволяет эффективнее использовать ресурсы и уменьшать количество серверов. Но важно задать себе вопрос: действительно ли у вас больше 10 000 одновременных запросов? Часто мы говорим о «хайлоаде», но на практике подобная нагрузка встречается далеко не у всех. Десятки тысяч RPS — это, скорее, специфичный кейс для нескольких крупных бигтех компаний. Даже у нас в облаке далеко не каждый сервис выходит на такой уровень. Благодаря большому опыту скорость разработки многопоточного кода выше, чем асинхронного.
Второй важный момент — это баланс CPU и IO операций. Многопоточность с No GIL начинает выигрывать именно там, где увеличивается доля CPU-операций. Это как раз тот случай, когда баланс сдвигается, например, с 20/80 в сторону 40/60. Чем больше у вас вычислений, тем больший прирост вы получите с многопоточностью после релиза No GIL. Cам по себе он не ускоряет обработку операций, а просто снимает ограничение на использование CPU несколькими потоками одновременно. Поэтому в сценариях с высокой нагрузкой на процессор это становится преимуществом. В случаях, когда баланс смещается в сторону IO — так бывает в случае работы с LLM агентами — проценты CPU и IO операций могут быть 5/95. В таком случае конечно же будет правильнее сразу смотреть на асинхронный подход.
Что выбираете вы для своих задач и проектов? Поделитесь в комментариях.
Комментарии (71)

MechanicZelenyy
06.10.2025 10:54Вся статья строиться на измерениях текущей реализации asyncio, учитывающей GIL и работающей в одном потоке, но no GIL так же позволит переписать планировщик корутин, что бы он использовал пулы потоков и тогда async станет эффективным и в CPU bound задачах (за счёт параллельного выполнения корутин в разных потоках из пула).
Вообще глобально многопоточность и асинхронность корутин это не много ортогональные вещи, но если говорить с точки зрения практики, но корутины всегда в пределе будут эффективнее потоков, так потоки управляются внешним планировщиком ОС, а корутины внутренним и имеют больше информации для оптимизации потока выполнения.
WLMike
06.10.2025 10:54Текущую реализацию очень сложно перевести в многопоточную - крайне мало вероятно, что это случится. Там слишком много завязано на то, что переключение может произойти только в очень определенных местах кода

MechanicZelenyy
06.10.2025 10:54Вообще говоря нет, кроме того можно сделать эрзац вариант уже в текущем интерпретаторе: берем интерпретатор в no-gil сборке, а потом в отдельных потока запускаем свои EventLoop, у меня получился параллельные asyncio, внутри одно eventloopа естественно всё последовательно, но корутины из разных eventloopов могут работать параллельно в разных потоках

WLMike
06.10.2025 10:54Ну, не так это. Если у вас есть некая переменная, к которой вы доступаетесь из разных корутин, то сейчас у вас есть гарантия, что ее никто не прочитает/поменяет между двумя async вызовами внутри одной корутины. И сейчас куча мест в коде, которые полагаются на это и не берут локи. В вашей реализации или в более полноценной эта гарантия ломается

MechanicZelenyy
06.10.2025 10:54И да, и нет.
Если говорить о переменных которые в контексте текущего диспатчера корутин, то так и останется в этом дистпатчере, а переменные которые имеют какой-то глобальный доступ, так я и сейчас могу запустить отдельный тред из которого могу поменять значение между двумя вызовами async.
Никаких новых требований не предполагается, да переменные который не потокобезопасны, они остаются не потокобезопасными, но они и так всегда по умолчанию не потокобезопасны.
Не надо делать не потокобезопасные вызовы, неважно из корутины они делаются или из мультитрединга.
WLMike
06.10.2025 10:54Так сейчас asyncio исполняется в одном потоке, и все это понимают и пишут в основном потоко не безопасно, но без лишних локов, где это можно. В описанном вами варианте изменения из другого потока, наверняка или не нужна атомарность, или лок повесят. А вот если вдруг внезапно asyncio полетит во множестве потоков, то куча существующего кода разнесет
Практически везде в существующем коде есть такие куски
async def some_method(self): res = await async_fn() self.some_attr = sync_fn(self.some_attr, res)И сейчас есть гарантия, что чтение атрибута и его обновление происходит атомарно, если специально не подпрыгивать с изменением его в другом потоке

MechanicZelenyy
06.10.2025 10:54Это тоже абсолютно решаемо, рас кажу как это сделано в Kotlin.
Там при запуске корутины можно указать диспетчер корутин (один из встроенных или даже свою реализацию), который определит какой пул потоков использовать, и там есть диспетчер который запускает корутину всегда в главном потоке, который запускает в параллельном пуле, в специально io пуле.
Так что просто оставляем что по дефолту asyncio.run запускает всё в одном потоке, но опцией даем возможность передать диспетчер
WLMike
06.10.2025 10:54Если это opt in решение будет, то конечно возможно. Но видимо этого все-таки не будет. Я только что слушал Соболева. Как я его понял, многим кор разработчикам не очень нравится сама концепция asyncio. И скорее не многопоточность будут тащить туда, а сделают чего-то green threads с отдельным api

IgorAnokhin Автор
06.10.2025 10:54Справедливости ради, доклад я заканчивал словами о том, что в многопоточную модель потенциально можно было бы привезти green-threads. А точнее, сделать сами потоки "асинхронными внутри". То есть на один поток операционной системы порождалось бы несколько виртуальных потоков. Каждый такой виртуальный поток был бы фактически асинхронной корутиной. Как в Java, Golang и других "честных" языках, где давно уже green-threads или аналоги.
Хотя нечто похожее мы фактически получаем и сейчас, запускаю несколько инстансов асинхронного приложения на UWSGI. У нас фактически есть несколько потоков, каждый из которых внутри поддерживает асинхронность.
Единственный нюанс - эти потоки не видят друг друга и не столь эффективно работают, как в том же Golang

alex88django_novice
06.10.2025 10:54А зачем? В питоне уже давно есть stackless корутины, ничего не мешает в будущем использовать их вкупе с многопоточным рантаймом при отсутствии GIL (см. растовый tokio). А грин-треды это в принципе иной подход к реализации многозадачности: pre-emptive вместо cooperative

alex88django_novice
06.10.2025 10:54Питонистов, привыкших к тому что есть GIL, заставили верить в то, что многопоточный и асинхронный подходы при работе с IO - конкурирующие и взаимоисключающие. Но это справедливо лишь в контексте существования GIL )
Условная M:N модель, где M асинхронных задач (тысячи их) склейлятся на N тредов ОС (ограниченное количество, как правило равное кол-ву ядер CPU) - это нормальная практика во многих ЯП.

AlexanderMelory
06.10.2025 10:54Замечательная async await модель в dotnet:
асинхронная задача освобождает текущий поток, когда она завершается, любой свободный поток ее подхватывает.
в эпоху no gil, полагаю, async await питона так же отойдет от парадигмы 1 асинхронный поток на 1 процесс

WLMike
06.10.2025 10:54Модель dotnet практически невозможно затащить в Питон - сломается весь существующий асинхронный код

GamePad64
06.10.2025 10:54Сломается и сломается. Напишут в breaking changes об изменениях и дадут время на адаптацию. Не в первый раз уже ломающие изменения были в python 3

WLMike
06.10.2025 10:54Кажется с python 3 наелись и больше такого не хоят

AlexanderMelory
06.10.2025 10:54no-gil тоже все ломает, и ничего, двигается прогресс, по тихоньку

WLMike
06.10.2025 10:54Он не все ломает. Вся нагрузка ложится на разработчиков языка и библиотек, которые сишный апи используют. Нормально продуктовый код на чистом Питоне не затрагивается

Grigo52
06.10.2025 10:54Проще не пытаться превратить Python в C#, а использовать сильные стороны каждого. В Python No GIL дает честную многопоточность для CPU-задач, для IO-оркестрации есть однопоточный asyncio. Комбинируя их, можно получить хороший результат, не ломая язык

IgorAnokhin Автор
06.10.2025 10:54Да, всё так. Спасибо за комментарий!
Хотя ветка обсуждения на введение green-threads активно сейчас ведётся, поэтому мб лет через 5 мы можем увидеть что-то такое. Вот ссылка, если интересно:
https://discuss.python.org/t/add-virtual-threads-to-python/91403

zzzzzzerg
06.10.2025 10:54Есть важный нюанс: планировщик операционной системы сам решает, когда выделить потокам время на выполнение. При этом GIL в CPython заставляет поток освобождать его примерно раз в 5 миллисекунд.
* For the longest time, the eval breaker check would happen * frequently, every 5 or so times through the loop, regardless * of what instruction ran last or what would run next. Then, in * early 2021 (gh-18334, commit 4958f5d), we switched to checking * the eval breaker less frequently, by hard-coding the check to * specific places in the eval loop (e.g. certain instructions). * The intent then was to check after returning from calls * and on the back edges of loops. * * In addition to being more efficient, that approach keeps * the eval loop from running arbitrary code between instructions * that don't handle that well. (See gh-74174.) * * Currently, the eval breaker check happens on back edges in * the control flow graph, which pretty much applies to all loops, * and most calls. * (See bytecodes.c for exact information.)
IgorAnokhin Автор
06.10.2025 10:54Могли бы вы пояснить, что хотели этим сказать?

zzzzzzerg
06.10.2025 10:54Это напоминание о моей глупости - я думал я понимаю как работает GIL, а оказалось не понимаю. Будет повод разобраться глубже.

amatoravg
06.10.2025 10:54А каковы на ваш взгляд перспективы async и free-threading в контексте появления в 3.14 субинтерпретаторов? Вот бы этот вариант ещё рассмотреть.

Grigo52
06.10.2025 10:54Спасибо за трезвый взгляд на вещи, хорошее доказательство того что выбор между async и threading это не религиозный вопрос, а инженерный компромисс, который зависит от конкретной задачи, нагрузки и... версии Python
Для многих проектов "просто подождать Python 3.13" реально может оказаться самой дешевой и эффективной стратегией

slonopotamus
06.10.2025 10:54А вот в случае с IO-bound задачами, на мой взгляд, оба подхода — многопоточность и асинхронность — дают схожий результат.
Ох уж эта дихотомия... Допустим, пишем эхо-сервер, который должен пережевать 10Gbps пакетами по 64 байта. Это I/O-bound или CPU-bound? Или таки оно при использовании неэффективного языка из I/O-bound внезапно превращается в CPU-bound?

IgorAnokhin Автор
06.10.2025 10:54Допустим, пишем эхо-сервер, который должен пережевать 10Gbps пакетами по 64 байта
А здорово вы это придумали, я даже сначала и не понял)
Если же серьёзно, то под каждую задачу есть свой инструмент. Вы привели в пример пограничное значение, которое маловероятно, что будет написано на Python в целом.
При этом базовый echo-server я бы тоже писал на Python. Тут возникает вопрос: "А почему echo-server должен пережевывать 10Gbps?". Если это ошибка в постановке ТЗ - разработчик должен заметить и сказать об этом бизнесу. Если такое придумал сам разработчик - он скорее всего начинающий и не знает, какая реальная будет нагрузка, поэтому закладывается на супер хайлод.
Во всей статье я говорил о реальных примерах и реальном использовании. Поэтому будьте реалистом)

slonopotamus
06.10.2025 10:54А почему echo-server должен пережевывать 10Gbps?
Чтобы задача была очевидно IO-bound. Если у нас там всего мегабайт в секунду всего, она ничем не bound.

yrub
06.10.2025 10:54Для меня асинхронность — это, в первую очередь, не про скорость, а про экономию ресурсов. Если у нас до 1000 одновременных запросов, выбор между многопоточностью и асинхронностью не критичен. Многопоточность займёт, например, 4 ГБ оперативной памяти, и это допустимо. Если же речь идёт о более чем 10 000 одновременных соединений, асинхронность действительно эффективнее.
на лицо полное непонимание терминов, в принципе это простительно, потому в питоне это никогда не работало и люди, которые не знают ничего кроме него начинают излагать домыслы, коих полон интернет.
асинхронность это про то, что вы можете взять и выполнить какой-то код в фоновом процессе относительно текущего. точка. он может быть как честный без GIL так и с ним. все, больше это ничего не значит. очевидно что если у вас нет GIL то вы тут получите преимущество, потому что вы будете использовать все ресурсы процессора, а не одно ядро.
заводя разговор про 10к запросов вы уже влазите уже в другую область, называется "конкурентность" - тут немного другие правила и требования. В принципе можете с этим базисом обращаться в чат гпт, он вам все разъяснит, в чем отличие конкурентности от параллелизма. ну и что такое асинхронность еще раз.

alex88django_novice
06.10.2025 10:54Если очень сильно обобщить, то "конкурентность" - это способность нескольких различных участков кода выполняться независимо друг от друга. Ваше определение "асинхронности" попадает под это понятие, и не понятно, зачем вы их разделяете. Параллелизм - это подвид конкурентности.
P.S. говоря об асинхронности, автор явно имеет в виду конкретно модель асинхронного IO в python, что очевидно
yrub
06.10.2025 10:54просто это точные определения того что делается и как, чтобы все правильно понимали о чем речь и какой проблеме.
параллелизм это когда вы несколькими исполнительными блоками вместе работаете над общей задачей. соответственно ваш алгоритм распараллеливается или нет в зависимости от того как он работает. самый простой вариант суммирование двух больших массивов поэлементно.
асинхронность - возможность отправить вычисления (в общем понимании) считаться куда-то еще и не тормозить текущее выполнение. Вот есть асинхронное IO когда вы говорите “я не хочу ждать операцию, давай дуй в фоновое исполнение” - заметьте здесь нет речи о том как мы будем получить результат, речь не про колбеки и тому подобное, важен сам факт что мы можем как-то скинуть задачу с себя и пойти заниматься другими вещами.
конкурентность - в русском языке удачно дословно переводится от слова "конкурировать", особая ситуация когда в каком-то месте вам нужно иметь дело с одновременно поступающими задачами, например запросами или потоками которые считают матрицу и теперь им всем нужно например увеличить общий счетчик. Вот эти потоки, запросы становятся конкурирующими, когда они лезут одновременной и конкурируют за какой-то общий ресурс. Потому что есть ситуации когда они не кокурируют.
цель параллелизма - ускорение за счет использования доп ядер, задача конкурентности - как-то обработать задачи вместе а не одну за одной.
Поэтому и получается что это все термины из примерно одной области, но лучше их применять по определению.

alex88django_novice
06.10.2025 10:54важен сам факт что мы можем как-то скинуть задачу с себя и пойти заниматься другими вещами.
согласен с данным тезисом. Однако, если у нас эта самая вычислительная задача не является фоновой, а является полноценной и равнозначной задачей со всеми прочими, разве не будет справедливым говорить об "асинхронном выполнении"?
В прочем, не важно. Это спор за термины, суть которых ясна

IgorAnokhin Автор
06.10.2025 10:54Вы рассуждаете об асинхронности с точки зрения разработчика и технологии. Мой же вывод об асинхонности с точки зрения компании и продукта. Зачастую, владельцу бизнеса не очень важно, асинхронность или многопоточность вы будете использовать. Он будет видеть счёт за вычислительные ресурсы.
А относительно скорости в 80% случаев подходы похожи. Поэтому нет разницы, что использовать. Да и относительно ресурсов, если быть честным, в 80% случаев разницы не будет особой тоже. Типичный паретто)

alex88django_novice
06.10.2025 10:54потребление ресурсов будет значительно отличаться, если речь идет о тысячах задач / тысячах тредов. И не важно, параллельно эти треды будут выполняться (без GIL) или конкурентно (c GIL). Отсутствие GIL - не silver bullet, параллелизм всей сисиемы все равно будет ограничен кол-вом процессорных ядер, а работа тысячи тредов приведет к значительным затратам ресурсов CPU.

KonstantinTokar
06.10.2025 10:54Вышел Python 3.14 . Одно из ожидаемых событий - та самая многопоточность. И она не работает в некоторых важных модулях, например для работы с базами данных. То есть пока вопрос что победит не стоит - побеждает асинхронность.
segment
Подскажите, а в чем заключается цель использования и усложнения кода на python, если он вообще не про производительность? Почему для backend не использовать обычные и более подходящие языки типа Go/C#/etc?
WLMike
Начинаете вы когда производительность не важна, а со временем она может стать важной. И перед вами выбор - переписать все на более быстрый язык, или добавить костылики внутри языка и увеличить скорость не переписывая. Для многих переписать все не вариант
segment
Но почему выбирают именно python для работы над задачей, которая заведомо не будет про "производительность"? На том же C# сделать API вроде не настолько сложнее, чем на python.
WLMike
Вы на старте не знаете нужна ли вам производительность - может через пол года ваш маленький проект умрет или всегда останется маленьким. При этом есть поверье, что на языках вроде Python, Ruby легче писать код с нуля за счет гибкости языка и библиотечек вроде рельсы или Джанго. Когда вы упретесь в производительность скорее всего будут ресурсы, как-то решить эту проблему
IgorAnokhin Автор
Ответил подробнее ниже
SerafimArts
Ну с таким же успехом можно взять и PHP, где есть какой-нибудь Symfony, который даст форы и RoR, и Django, а по скорости можно выжать производительность уровня gcc -o2.
Так что, имхо, язык выбирается не по критериям "удобство"/"скорость"/"экосистема" и проч., а исключительно по двум:
У нас есть команда, которая знает язык Х (в 99% случаях непрофильный специалист взяв язык из другой области превратит его код в лютейший неподдерживаемый трешак)
И "потому что можем"
WLMike
php вполне вариант, и команда важна. Но есть много случае, когда команды просто нет - нужно выбрать стек и команду, и тут часто выбор клонится в сторону языков вроде руби, пыхи и питона
KonstantinTokar
Единственная российская популярная программная система работает как раз на PHP. Неожиданно.
KonstantinTokar
А у вас есть 100 программистов на питоне и один на С# - то есть на ранних стадиях выбирается питон, а потом оказывается что всё сделано и работает и переделывать никто не хочет. И на С# писать вообще безумие, я бы ещё понял C++, но это детали.
segment
Почему безумие?
KonstantinTokar
Чисто моё убеждение, основанное на представлении что C# это однозначно Windows - что для меня неприемлемо, а политика нашей страны вполне однозначно требует перехода на линукс (сейчас это не так и C# и пол линуксом работает, но этому "сейчас" всего несколько лет). А сам Microsoft решил сделать движение в Rust для Office364, да ещё и заявил что C# не под угрозой. Ну и пресловутый вендор-лок.
segment
Ну с появлением .net core это уже неактуально. Честно не совсем понимаю о каком вендор-локе идет речь, можете пояснить?
KonstantinTokar
Сразу скажу - на C# я не программирую.
.NET выпускается Microsoft.Тесно связана с другими проектами Microsoft. Это и есть вендор-лок. (страничка на гитхабе с моей точки зрения, не то на чём держится огромная экосистема). Веры в то, что система начавшая жизнь в windows будет работать в linux у меня очень мало. Да, я запускаю многие windows программы в linux, некоторые даже лучше работают.
Ну и количество открытых проектов на c# о которых я слышал равно нулю, в отличии от С++, Java и Питон - о чём то это говорит (ну может, конечно, и об узости моего кругозора или моей предметной области).
segment
Это немного устаревшая информация. Можете посмотреть тут https://dotnet.microsoft.com/en-us/platform/open-source
KonstantinTokar
Ну опенсурс, кто подписывается под выпуском очередной версии?
segment
Это разве имеет такое большое значение для open source? Мне кажется, что очень хорошо, что такая большая компания развивает open source.
XVlady5
Имеет. C#, go это жёсткий вендор лок. Ими корпорация крутит как хочет. Причём не просто может, а делает. Посмотрите на яблоко - там все тоже, но быстрее.
segment
Не совсем понимаю, в чём заключается "кручение"? В обычном open source владелец проекта может управлять разработкой, тот же Линус крутит как хочет. Можете привести пример?
KonstantinTokar
Преимущества C# проявляются когда задействована инфраструктура Microsoft. А пока её нет, helloworld проще написать на питоне.
segment
Когда C# сервер net core работает под linux, то какая инфраструктура Microsoft подразумевается? Для питона мне тоже runtime нужно установить.
KonstantinTokar
Я думаю, что лучше этот вопрос задать практикующим программистам под Linux и сравнить с ответами на тот же вопрос программистов под windows. Стек - это системы типа Azure или Jenkins
ccapt
можно подумать, что чистые опен-сорс проекты зависят от вашего мнения больше. точно так же возможны повороты разработки, просто они определятся не менеджерами майкрософта, а мантейнерами проекта. вы от этих решений далеки одинаково, а влияют они не слабее.
KonstantinTokar
Вот хороший пост в обсуждении про C# - https://www.reddit.com/r/csharp/comments/1n0v8qq/comment/nawyfyl/?tl=ru&utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
IgorAnokhin Автор
Да, @WLMike прав. Зачастую, когда ты начинаешь свой небольшой проект - проще и быстрее взять язык, для которого много разных библиотек и на котором можно написать "быстро".
Затем ты выходишь на рынок и начинаешь это тестировать. И дальше, если мы говорим о каких-нибудь Pet проектах - проект так и останется маленьким. Или не найдёт свою аудиторию. Так будет в 80% случаев.
А если всё-таки проект вырастет - уже поздновато менять весь стек. Нужно расширять штат. А имеющийся штат только на Python пишет. Да и найти резработчиков - не просто. Поэтому выбирается путь думать об асинхронности и закидывать всё железом)
А если сразу писать всё на C# или чём-то производительном -проект может и не выйти. Или опоздать с моментом выхода. Тот же Golang, на который уходят с Python - сильно многословнее получается.
segment
Но ведь даже в своем проекте "быстро" довольно относительно. Отладка в python довольно посредственная, работа с типами муторная и подвержена ошибкам, та же обработка ошибок потребует времени. Это всё как-то не похоже на "быстро", хотя, наверное в это вкладывается что-то другое. По наличию библиотек, скорее всего, — да, тут python будет лидировать. Тот же ChatGPT может сейчас довольно много всего написать рабочего. Но если речь про какой-нибудь backend, то и на других языках вроде нет проблем с различными библиотеками. В том же Visual Studio nuget пакеты ставятся в пару кликов.
alex88django_novice
Отладка в python просто замечательная
Почему не использовать Go? Чтобы проект с более-менее сложной бизнес-логикой на выходе не был x3 размером по кодовой базе от того, что могло бы быть, используя python :)
user-book
о да, размер кодовой базы это очень важный параметр для серверного приложения
особенно забавно это слышать для ситуаций на долгую дистанцию, где через 2-3 года вы охренеете поддерживать свой зоопарк на питоне (потому как вряд ли вы будете держать все актуально и обновлять)
alex88django_novice
Важный параметр - это поддерживаемость кода, верно.
Вопрос: почему я должен охренеть поддерживать приложение на пайтоне, но не охренеть поддерживать аналогичное приложение на go?
user-book
нормальные модули и версионность. Тебя вообще не колышет что там по модулям, если оно собирается то оно будет собираться и через 5 лет без танцев и тд.
строгая типизация при максимально простом языке. Это не только компактные шустрые бинарники, но и читаемость кода пополам с безопасностью
Мультиплатформенность. Питон вроде тоже мультиплатформенный но с кучей оговорок, если надо собрать под несколько разных платформ то зачастую цирк с конями когда одни питоновские пакеты используются для компиляции другого пиновского кода (и у этого зоопарка так же есть версионность и особености)
Для себя на практике вывел такие наблюдения для серверного:
php нестареющая классика, но сейчас он стал тем против чего боролся и на долгой дистанции выходит дорого, сложно и ограничено
JS это из мира фулстеков, с кучей приколов на уровне ноды (приколы чем то схожие с питоном). В отличии от php с раскаткой могут быть проблемы на долгой дистанции, особенно если обновы набегами раз в два года. Так же язык позволяет писать вообще нечитаемые ужасы, куда уж там php
python это старший брат JS. Проблемы практически те же, выгода побольше для бека. На долгой дистанции ВСЕГДА будет ебля со сборками. Даже в ноде можно привязаться к конкретным репозиториям и версиям, но в питоне все это сложно и обычной практикой является иметь отдельный докер под сборку и встраиваемую папку со всеми либами.
Goland это просто, максимально просто. Со сборкой - один раз настроил паплайн и забыл (у меня есть боевые проекты которые все еще дописываются по мере на 1.8). Написание так же просто - люди с базовыми знаниями вкатятся очень быстро, и даже без знания читать код может даже тот кто языка вообще не знает. Он шустрый потому прощает костыли, у того же питона вава с производительностью особенно если косячить, но в Го со сторогой типизацией творить хуйню можно, но не в таких широких пределах.
Раст то же что Си и С++. Можно многое, надо знать, нужно ебатся, код для не знающего похож на ельфийский - тот же php или js можно научится читать хотя бы примерно за день
alex88django_novice
про "читаемость", в python есть type annotations, и кода без оных я не наблюдах в коммерческих проектах последних лет 5 уж точно.
Для типобезопасности есть линтеры (с оговорками конечно но все же) которые, как правило, тоже must have
Про проблемы с кроссплатформенностью пайтона - тру стори, только в 99% случаев все запускается на одной платформе
Что мешает привязаться к конкретным версиям в питоне?
И что, в Go-либах не бывает breaking changes?
user-book
С Го есть целая секта которая верит в наглядность кода, так что не нужны комментарии) Секстанты, но в чем то они и правы - в питоне можно создать ужасные вещи (тот же esptool) в которых без IDE и понимания языка далеко не уйдешь в понимании. В го сотворить нечитаемое можно только если пилить однофайлово, в единой функции и часто юзать GOTO. Хотя там есть свои подводные с тем же init() но это можно отслеживать (к слову отладка и тесты с бенчмарками на Го еще одна причина его юзать)
С либами есть файл go.mod в котором четко прописывается используемая версия Го и все используемые модули, которые так же указываются с версией (и в модулях то же самое). То есть если заюзать модуль который конфликтует с текущей версией то это сразу напишет. Если же все ок собирается, то и через 20 лет оно для сборки будет использовать модули конкретных версий даже если там набежало еще 100500 версий (указывать last нельзя, точнее загрузить или обновить можно, но в go.mod указывается конкретная версия).
Работа с Го это лучшее что я встречал за 15 лет практики. Как сборка, так и всякие мелочи по типу работы с модулями или тесты/бенчмарки. Встраивать код из других языков, профилировать или прописывать прямо в коде что именно подтягивать при конкретных сборках (как в си) - идеально. И как вишенка язык простой, голова отдыхает, ты думаешь о функционале, а не о синтаксисе, причем все достаточно низкоуровнево при желании.
alex88django_novice
синтаксис - дело наживное, вопрос опыта и привычки, а прописать конкретные версии в python’е тоже можно :) Если для вас язык, где дженерики завезли в N-ной версии - это «лучшее» то предлагаю дискуссию закрыть )
user-book
хз хз за дженерики
много про что могу сказать что появилось с 1.13 версии или 1.8 хотя по уму должно быть по умолчанию, но вот дженерики не входят в это число
Бысто что то накидать проверить концепцию ок, но вот если боевое и боги упаси высоконагруженное то генераторы ван лов
Отлаживать дженерики та еще лажа, особенно по ресурсам. Особенно учитывая что в реальности применяется вилка до 4 типов максимум, покрытие всех any это уже что-то кривое и небезопасное.
И опять таки 90% того что в других языках решают дженериками, решает правильное использование интерфейса. Прямо что бы напрямую разные типы юзается только с разными мапами чтоб сортировать разве что.
alex88django_novice
для меня код на go похож на эльфийский, а код на расте - вполне читаемый и понимаемый. Так что тут субъективщина чистой воды.
Про "много нужно знать" - при работае с любым ЯП нужно знать, как этот ЯП работает, хотя бы базово.
И, в целом, все что вы написали, ни разу не отменяет моего основного тезиса, сказанного выше. Если язык подразумевает тонны бойлер-плейта по своей сути, то для большего, чем написания небольших приложений он не годится.
KonstantinTokar
Если смотреть на читаемость, то самый читаемый (по моему опыту) это грамотно написанный проект на Java, С++ вполне читаем если немного вникнуть. А вот Питон, к примеру, набит абсурдными конструкциями и сахаром для компенсации архитектурных недоработок, JS вообще критиковался всеми за всё , PHP я уже не видел лет 20 и ничего о нём сказать не могу.
S0mbre
Я делал бэки и на Python и на Go. Мне трудно сказать за читаемость или что можно где-то "ужасные вещи" натворить, поскольку если с головой подойти к кодингу и просто знать соответствующий ЯП, то все будет максимально понятно и вовсе не ужасно. А ужасные вещи можно наколбасить с любым ЯП и на том же Go, я вас уверяю)) Да, Go выглядит как будто более приспособленным для веб серверов, но так он для них и создавался на секунду. Но что касается читаемости, попробуйте почитать код с десятками каналов и разобрать логику. Если не на опыте, с каналами и горутинами тоже можно натворить всякого.
Grigo52
Потому что производительность CPU лишь одна из метрик. Есть еще скорость разработки, доступность библиотек и стоимость найма разработчиков.
Python часто выигрывает по этим трем пунктам, что для многих бизнес-проектов на старте важнее чем голые RPS