
Недавно на Youtube появилась документалка о Python. Примерно в середине ленты есть драматический эпизод о том, как переход от Python 2 к 3 разделил сообщество (спойлер: в конечном итоге этого не случилось).
Первые версии Python 3 (3.0-3.4) в основном делали упор на стабильность и упрощение перехода пользователей с версии 2.7. В 2015 была выпущена версия 3.5 с новой фичей: ключевыми словами async
и await
для выполнения корутин.
Миновало десять лет и девять релизов, через считанные недели выпустят финальную версию Python 3.14.
Пока все отвлеклись на фичи разноцветного REPL в 3.14, в release notes появились серьёзные заявления, связанные с конкурентностью и параллелизмом.

Обе эти фичи — огромный шаг вперёд в том, как Python можно будет использовать для выполнения конкурентного кода. Но если async
с нами уже десять лет, зачем они нам понадобятся?
Основной сценарий использования async
— это веб-разработка. Корутины хорошо подходят для внепроцессных сетевых вызовов, например HTTP-запросов и запросов к базам данных. Мы же не будем блокировать целиком интерпретатор Python, пока на другом сервере выполняется SQL-запрос?
Однако в трёх наиболее популярных веб-фреймворках Python поддержка async
по-прежнему не универсальна. FastAPI асинхронный изначально, в Django есть частичная поддержка, но в ключевых областях наподобие ORM (база данных) работа над поддержкой async всё ещё продолжается. Flask синхронный и, вероятно, останется таким навсегда (Quart — его async-альтернатива с похожими API). SQLAlchemy, самый популярный ORM для Python, добавил поддержку asyncio только в 2023 году (changelog).
Я задал вопрос «async не так популярен?» паре разработчиков, чтобы узнать их мнение.
Кристофер Трюдо, один из ведущих подкаста Real Python, сказал следующее:
Определённые виды ошибок отлавливаются компилятором, а другие просто пропадают. Почему не выполнилась эта функция? Ой, я забыл сделать для неё await. Ошибка в корутине? А ты точно запустил её с нужными параметрами? Для меня по-прежнему проще разбираться с потоками.
Майкл Кеннеди поделился своими рассуждениями:
[GIL] настолько вездесущий, что большинство пользователей Python так и не выработало мышление в концепциях многопоточности/async. Так как async/await работают только на границах ввода-вывода, но не в CPU, то они гораздо менее полезны. Например, можно использовать их в вебе, но большинство серверов всё равно распределяют задачи между 4-8 веб-воркерами.
Так что же здесь происходит и можно ли применить уроки Free-Threading и множественных интерпретаторов в 3.14, чтобы спустя ещё десять лет мы обернулись назад и задумались, почему они не так популярны?
Проблема 1: в чём заключается задача асинхронности?
Корутины полезнее всего в задачах, связанных с вводом-выводом. В Python можно запускать сотни корутин для выполнения сетевых запросов, а затем ожидать, пока все они завершатся, без необходимости запускать их по одной за раз. Концепция корутин довольно проста: у нас есть цикл (цикл событий), и мы передаём ему корутины для выполнения.
Вернёмся к классическому сценарию использования — к HTTP-запросам:
def get_thing_sync():
return http_client.get('/thing/which_takes?ages=1')
Эквивалентная async-функция понятна и читаема:
async def get_thing_async():
return await http_client.get('/thing/which_takes?ages=1')
Если вызвать функцию get_thing_sync()
и await get_thing_async()
, они займут одинаковое количество времени. То, что мы вызываем их «✨ асинхронно✨» не делает их волшебным образом быстрее. Мы получаем выигрыш, только когда одновременно выполняется несколько корутин.
При получении множества HTTP-ресурсов можно запустить все запросы одновременно через сетевой стек операционной системы, а затем обрабатывать каждый запрос при его получении. Важно здесь то, что настоящая работа — отправка пакетов и ожидание удалённых серверов — происходит вне вашего процесса Python, пока код находится в состоянии ожидания. Async в этом случае наиболее эффективен: мы запускаем операции, получаем дескрипторы для ожидания (task/future), а цикл событий эффективно уведомляет корутину о завершении каждой операции, не тратя ресурсы CPU на busy‑polling.
Этот сценарий хорошо работает, потому что:
Удалённая точка обрабатывает задачу в другом процессе.
Локальная точка (HTTP-библиотека asyncio) может получить управление, пока ожидает ответа.
У операционных систем есть стеки и API для работы с сокетами и сетью.
Всё это замечательно, но я начал с того, что корутины наиболее ценны в задачах, связанных с вводом-выводом. Затем я выбрал одну задачу, с которой asyncio справляется очень хорошо (HTTP-запросы).
А как насчёт ввода-вывода с диска? У меня гораздо больше приложений на Python, считывающих и записывающих файлы на диски или в память, чем выполняющих HTTP-запросы. Ещё у меня есть программы на Python, запускающие другие программы при помощи subprocess
.
Можно ли превратить всё это в async
?
На самом деле, нет. Цитата из asyncio Wiki:
asyncio не поддерживает асинхронные операции с файловой системой. Даже если файлы открываются с O_NONBLOCK, чтение и запись будут блокироваться.
Решение заключается в использовании стороннего пакета aiofiles
, дающего нам возможность асинхронного файлового ввода-вывода:
async with aiofiles.open('filename', mode='r') as f:
contents = await f.read()
То есть наша миссия выполнена? Нет, потому что для выгрузки блокирующих файловых операций ввода-вывода aiofiles
использует пул потоков.
Побочный квест: почему файловый ввод-вывод не асинхронный?
В Windows есть API асинхронного файлового ввода-вывода IoRing. В новых ядрах Linux эта возможность реализуется при помощи io_uring
. Единственная качественная реализация io_uring
для Python, которую мне удалось найти —это синхронный API, написанный на Cython.
Существуют API io_uring для других платформ, в Rust есть реализации на основе tokio, для C++ есть Asio, а для Node.JS — libuv.
То есть asyncio Wiki немного устарела, но
Большинство продакшен-приложений на Python работает в Linux, где реализацией является
io_uring
io_uring
настолько была подвержена проблемам безопасности, что RedHat, Google и другие разработчики ограничили или удалили её. Выплатив миллион долларов баг-баунти за уязвимости, связанные сio_uring
, Google отключила её для некоторых продуктов. Проблема была серьёзной; во многих отчётах баг-баунти описывались эксплойты io_uring.
Поэтому нам придётся ещё немного придержать коней. В операционных системах уже есть API файлового ввода-вывода, обрабатывающий потоки для конкурентного ввода-вывода. Пока он вполне справляется со своей работой.
Подведём итог: утверждение «корутины наиболее ценны в задачах, связанных с вводом-выводом» справедливо только для сетевого ввода-вывода, а сетевые сокеты в Python никогда не блокировали операции. Открытие сокета в Python — это одна из немногих операций, освобождающих GIL, и она работает конкурентно в пуле потоков, как неблокирующая операция.
Напоминание: какие async-операции есть в asyncio?
Операция |
Asyncio API |
Описание |
---|---|---|
Sleep |
Асинхронный sleep в течение заданного времени. |
|
Потоки TCP/UDP |
Открывает соединение TCP/UDP. |
|
HTTP |
Асинхронный HTTP-клиент. |
|
Выполнение подпроцессов |
Асинхронное выполнение подпроцессов. |
|
Очереди |
Реализация асинхронных очередей. |
Проблема 2: как бы быстро мы ни бежали, нам не убежать от GIL
Уилл Макгуган, автор Rich, Textualize и множества других крайне популярных библиотек Python, изложил свою точку зрения на async:
Мне очень нравится async-программирование, но оно не так интуитивно понятно для большинства разработчиков, не имевших опыта в написании сетевого кода. В Textual я наблюдаю следующую часто возникающую проблему: разработчики тестируют конкурентность, добавляя вызов a
time.sleep(10)
, чтобы симулировать планируемую ими работу. Разумеется, это блокирует весь цикл. Но подобный класс проблем сложно объяснить разработчикам, не особо работавших с async. Например, что означает «блокировка» кода, когда необходимо полагаться на потоки. Без этого фундамента знаний async-код будет непослушным, но не поломается. Поэтому разработчики не получают быстрых итераций и обратной связи, которую мы ожидаем от Python.
Разобрав ограниченность сценариев применения async, можно перейти к другой трудности: GIL Python.
Когда я работал над проектом «моста» между C# и Python под названием CSnakes, одним из самых сложных аспектов оказывался async.
C#, язык, из которого позаимствован синтаксис async
/await
, имеет более широкую поддержку async в базовых библиотеках ввода-вывода, потому что он реализует Task‑based Asynchronous Pattern (TAP), где задачи диспетчеризируются в управляемом пуле потоков. Операции ввода-вывода с дисками, сетью и памятью обычно имеют и асинхронные, и синхронные методы.
На самом деле, реализация асинхронности в C# проходит весь путь от диска до высокоуровневых API, например библиотек сериализации. Десериализация JSON выполняется асинхронно, с XML та же ситуация.
У модели async C# и модели async Python есть важные различия:
C# создаёт пул задач и задачи планируются в этом пуле. Среда исполнения автоматически управляет количеством потоков.
Циклы событий Python принадлежат создавшему их потоку. Задачи C# могут планироваться любым потоком.
async-функции Python — это корутины, планируемые в цикле событий. async-функции C# — это задачи, планируемые в пуле задач.
Преимущество модели C# заключается в том, что Task
— абстракция более высокого уровня, чем поток или корутина. Это значит, что разработчику не приходится беспокоиться о внутреннем управлении потоками, он может планировать множество задач для конкурентного ожидания или выполнять их параллельно при помощи Task Parallel Library (TPL).
В Python «цикл событий выполняется в потоке (обычно в основном потоке) и исполняет все обратные вызовы и задачи в своём потоке. Пока задача выполняется в цикле событий, в том же потоке не может выполняться ни одна другая задача. Когда задача выполняет await-выражение, запущенная задача приостанавливается, а цикл событий выполняет следующую задачу»1.
Вернёмся к комментарию Уилла «разумеется, это блокирует весь цикл»: он имеет в виду операции внутри async-функций, которые являются блокирующими, а потому блокируют весь цикл событий. Как мы говорили в Проблеме 1, это практически всё, за исключением сетевых вызовов и sleep.
При работе с GIL Python неважно, запущен ли у вас один поток или десять: GIL заблокирует всё, чтобы одновременно работал только один.
Некоторые операции не блокируют GIL (например, файловый ввод-вывод), и в таких случаях мы выполняем их в потоках. Например, если мы используем потоковую фичу httpx
для потоковой передачи большого сетевых данных на диск:
import httpx
import tempfile
def download_file(url: str):
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
with httpx.stream("GET", url) as response:
for chunk in response.iter_bytes():
tmp_file.write(chunk)
return tmp_file.name
то ни потоковый итератор httpx
, ни tmp_file.write
не блокируются GIL, поэтому есть выгода от их выполнения в отдельных потоках.
Мы можем объединить это поведение с asyncio API, воспользовавшись функцией run_in_executor()
цикла событий и передав ей пул потоков:
import asyncio
import concurrent.futures
async def main():
loop = asyncio.get_running_loop()
URLS = [
"https://example.place/big-file-1",
"https://example.place/big-file-2",
"https://example.place/big-file-3",
# etc.
]
tasks = set()
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool:
for url in URLS:
tasks.add(loop.run_in_executor(pool, download_file, url))
files = await asyncio.gather(*tasks)
print(files)
Мне не сразу очевидно, в чём преимущество такой схемы над выполнением пула потоков и вызовом pool.submit
. Мы сохраняем доступ к async API, поэтому если это важно, то можно решить проблему таким интересным образом.
Я считаю, что запоминание, документирование и объяснение того, что «блокируется» в Python, а что нет, сбивает с толку; к тому же ситуация постоянно меняется.
Фича free-threading делает asyncio более полезным или ненужным?
В Python 3.13 появилась очень нестабильная сборка Python с free-threading: в ней удалён GIL и заменён на меньшие, более компактные блокировки. Сводную информацию о параллелизме можно посмотреть в моём докладе с PyCon US 2024. Сборка 3.13 была недостаточно стабильной для какого-либо применения в продакшене. 3.14 выглядит гораздо более совершенным, и я думаю, что в 2026 году можно начинать внедрять free-threading в каких-то узких, хорошо тестируемых сценариях.
Важное преимущество корутин перед потоками заключается в том, что они занимают меньше памяти, имеют меньший оверхед переключения контекста и меньшее время запуска. Кроме того, async API проще в освоении.
Так как использующий потоки параллелизм в Python всегда был очень ограниченным, API в стандартной библиотеке довольно рудиментарны. Думаю, после стабилизации free-threading существует возможность внедрения в стандартную библиотеку API параллелизма на уровне задач.
На прошлой неделе я писал функцию, выполняющую две отдельные задачи. Одна вызывает очень медленный синхронный API, другая вызывает множество асинхронных API.
Мне требовалось следующее поведение:
Обе запускаются одновременно.
В случае сбоя одной она отменяет другую и генерирует исключение с информацией об исключении сбойной функции.
Результат комбинируется, только если обе завершаются успешно.

Так как задачи всего две, я не хочу, чтобы нужно было определять пул потоков или задавать количество воркеров. Также я не хочу отображать или собирать вызывающие стороны. Мне нужно сохранить информацию о типизации, чтобы получившиеся переменные строго типизировались из возвращаемых типов function_a
и function_b
. По сути, это такой API:
import tpl
def function_a() -> T1:
...
def function_b() -> T2:
...
result_a: T1, result_b: T2 = tpl.invoke(function_a, function_b)
На данный момент всё это возможно, но есть множество ограничений, связанных с GIL. Free-threading повысит популярность параллельного программирования на Python, и нам придётся пересмотреть некоторые API.
Проблема 3: мейнтейнинг двух API — это сложно
Я мейнтейнер пакетов, поэтому могу сказать, что поддержка и синхронных, и асинхронных API — сложная задача. Надо выбирать, где вы будете поддерживать async. Основная часть stdlib нативно не поддерживает async (например, бэкенды логирования).
Magic-методы Python (__dunder__
) не могут быть асинхронными. Например, не может быть асинхронным init
, поэтому никакой ваш код не сможет использовать сетевые запросы в инициализаторе.
Async-свойства
Это странный паттерн, но чтобы проиллюстрировать свою точку зрения, приведу простой пример. У нас есть класс User
со свойством records
. Это свойство возвращает список записей этого пользователя. С синхронным API всё просто:
class User:
@property
def records(self) -> list[RecordT]:
# лениво получаем записи из базы данных
...
Можно даже использовать лениво инициализируемую переменную экземпляра, чтобы кэшировать эти данные.
Портировать этот API в асинхронную версию будет трудно: хотя методы @property
могут быть асинхронными, стандартные атрибуты такими быть не могут. Если приходится выполнять await
одних атрибутов экземпляра без ожидания других, то API будет очень странным:
class AsyncDatabase:
@staticmethod
async def fetch_many(id: str, of: Type[RecordT]) -> list[RecordT]:
...
class User:
@property
async def records(self) -> list[RecordT]:
# лениво получаем записи из базы данных
return await AsyncDatabase.fetch_many(self.id, RecordT)
При каждом доступе к этому свойству его необходимо ждать:
user = User(...)
# единственный доступ
await user.records
# if
if await user.records:
...
# генератор списка?
[record async for record in user.records]
Чем глубже мы будем уходить в эту реализацию, тем больше вероятность того, что пользователь случайно забудет выполнить await свойства и произойдёт сбой без уведомлений.
Дублированные реализации
Огромный проект Azure Python SDK поддерживает и sync, и async. Мейнтейнинг обоих обеспечивается большой инфраструктурой кодогенерации. Это приемлемо для проекта, которым занимаются десятки разработчиков на полной ставке, но в случае чего-то маленького или волонтёрского для создания async-версии придётся вручную копипастить большую долю кодовой базы. А затем нужно будет патчить и выполнять обратное портирование исправлений и изменений между двумя версиями. Различия (в основном из-за вызовов await
) достаточно велики, чтобы запутать Git. В прошлом году я занимался код-ревью некоторых реализаций langchain, в которых были и синхронные, и асинхронные реализации. Каждый метод просто копипастился с незначительными различиями в поведении и собственными багами. Разработчики отправляли PR устранения багов для одной реализации, но не для другой, поэтому мейнтейнеры не могли мерджить их напрямую, а вынуждены были портировать исправление, игнорировать его или просить контрибьюторов реализовывать обе версии.
Фрагментация бэкенда
Так как мы в основном говорим о HTTP/сетевом вводе-выводе, нам также нужно выбрать бэкенд для sync и async. Для синхронных вызовов HTTP подходящими бэкендами будут requests
и httpx
. Для async
это будут aiohttp
и httpx
. Так как ни один не входит в стандартную библиотеку Python, внедрение и поддержка для основных платформ CPython рассинхронизирована. Например, на сегодняшний день у aiohttp
нет ни Python 3.14 wheels, ни поддержки free-threading. Альтернативная реализация цикла событий под названием UV Loop не имеет поддержки Python 3.14 и поддержки Windows .(Python 3.14 пока не вышел, так что отсутствие поддержки в обоих опенсорсных проектах вполне логично).
Оверхед при тестировании
Вслед за оверхедом копипастинга для мейнтейнера идёт тестирование этих API. Для тестирования async-кода требуются другие моки и другие вызовы, а в случае Pytest — ещё и целое множество расширений и паттернов для тестовых конфигураций. Эта ситуация так сбивает с толку, что я написал о ней пост, и он стал одним из самых популярных в моём блоге.
Вывод
Подведём итог: я считаю, что список возможных сценариев использования asyncio ограничен (в основном по причинам, над которыми asyncio
не имеет контроля), из-за чего страдает его популярность. Мейнтейнинг дублирующихся кодовых баз — сложная задача.
Веб-фреймворк FastAPI, изначально создававшийся асинхронным, снова вырос в популярности с 29% до 38% среди веб-фреймворков Python, заняв первое место. Его скачивают больше ста миллионов раз в месяц. Учитывая то, что основной сценарий использования async — это HTTP и сетевой ввод-вывод, первое место async-фреймворка означает успех asyncio.
Я считаю, что в версии 3.14 функции исполнителей субинтерпретаторов и free-threading сделают сценарии применения параллельности и конкурентности более практичными и полезными. Для них нам не нужны need async
API, что устраняет многие из проблем, перечисленных в этом посте.
Комментарии (9)
ititkov
04.09.2025 12:45Наконец-то нормальным языком, спасибо! Мне ChatGPT во время вайб-кодингового упражнения вдруг в середине начал предлагать async в тех местах, где обычно раньше реализовывал через await и не мог нормально объяснить почему и зачем.
aax
04.09.2025 12:45Все бы хорошо, но при преходе с версиии на версию нужные тебе библиотеки почти всегда отваливаюттся, деже если это экосистема pip.
CloudlyNosound
04.09.2025 12:45Асинхронщину не все понимают. Плюс, она не всем нужна. Автор оригинала замечательной статьи конечно не прочтет, но хоть тут останется.
ValeryIvanov
04.09.2025 12:45Почему не выполнилась эта функция? Ой, я забыл сделать для неё await.
У некоторых разработчиков кривые руки, раз они забывают сделать await.
Ошибка в корутине? А ты точно запустил её с нужными параметрами?
Ого, корутину можно запустить с неправильными параметрами... как и обычную функцию... как и всё что угодно.
asyncio не поддерживает асинхронные операции с файловой системой. Даже если файлы открываются с O_NONBLOCK, чтение и запись будут блокироваться.
Асинхронная работа с диском непосредственно в ОС реализована плохо, что каким-то образом является недостатком asyncio по сравнению с синхроным/многопоточным подходом.
В Textual я наблюдаю следующую часто возникающую проблему: разработчики тестируют конкурентность, добавляя вызов a time.sleep(10), чтобы симулировать планируемую ими работу.
У некоторых разработчиков кривые руки, ведь они в асинхронных функциях пишут синхронный time.sleep().
C#, язык, из которого позаимствован синтаксис async/await, имеет более широкую поддержку async в базовых библиотеках ввода-вывода, потому что он реализует Task‑based Asynchronous Pattern (TAP), где задачи диспетчеризируются в управляемом пуле потоков.
Здесь всё верно. Асинхронность в питоне плохо вплетена в язык. Многие модули со временем получают асинхронные альтернативы (например, subprocesses), но этого мало.
Я мейнтейнер пакетов, поэтому могу сказать, что поддержка и синхронных, и асинхронных API — сложная задача.
Это действительно так. Разработчики библиотек в принципе всегда страдают, так как им нужно поддерживать кучу платформ, языков, парадигм разработки. Обычные пользователи языка, коих большинство, обычно пишут либо только синхронный код, либо асинхронный с впиливанием многопоточности/многопроцессорности и не страдают от этой проблемы.
Лично я уже слабо себе представляю разработку на питоне без асинхронности, но как уже написали выше:
Асинхронщину не все понимают. Плюс, она не всем нужна.
andreymal
04.09.2025 12:45SQLAlchemy, самый популярный ORM для Python, добавил поддержку asyncio только в 2023 году
Накостылировал через greenlet
Мой внутренний перфекционист отказывается считать это поддержкой asyncio
GamePad64
04.09.2025 12:45"Ещё один всё понял"
В Python и Rust есть проблема красно-синих функций. Асинхронный и синхронный код — два разных плохо совместимых мира. Что с этим делать и как жить — непонятно. Есть предложения различной степени удачности.
homm
04.09.2025 12:45Первые версии Python 3 (3.0-3.4) в основном делали упор на стабильность и упрощение перехода пользователей с версии 2.7.
Те кто застал эти версии сейчас тихонько посмеялись.
tumbler
По Фрейду оговорочка)
По-моему зря это они. Раньше, написав код без использования async/await, можно было надеяться, что в середину твоего `a += 1` никто не вклинится. А теперь придётся задумываться над каждой строкой, как в Go.
outlingo
Вот только если не изменяет память, то "почему-то" лучше всегда считалось не "надеяться" а писать потокобезопасно.