Недавно на 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 появились серьёзные заявления, связанные с конкурентностью и параллелизмом.

Colorful REPL
Colorful REPL
  1. PEP779: официальная поддержка Free-Threading.

  2. PEP 734: несколько интерпретаторов в stdlib

Обе эти фичи — огромный шаг вперёд в том, как 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.

Этот сценарий хорошо работает, потому что:

  1. Удалённая точка обрабатывает задачу в другом процессе.

  2. Локальная точка (HTTP-библиотека asyncio) может получить управление, пока ожидает ответа.

  3. У операционных систем есть стеки и 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 немного устарела, но

  1. Большинство продакшен-приложений на Python работает в Linux, где реализацией является io_uring

  2. io_uring настолько была подвержена проблемам безопасности, что RedHat, Google и другие разработчики ограничили или удалили её. Выплатив миллион долларов баг-баунти за уязвимости, связанные с io_uringGoogle отключила её для некоторых продуктов. Проблема была серьёзной; во многих отчётах баг-баунти описывались эксплойты io_uring.

Поэтому нам придётся ещё немного придержать коней. В операционных системах уже есть API файлового ввода-вывода, обрабатывающий потоки для конкурентного ввода-вывода. Пока он вполне справляется со своей работой.

Подведём итог: утверждение «корутины наиболее ценны в задачах, связанных с вводом-выводом» справедливо только для сетевого ввода-вывода, а сетевые сокеты в Python никогда не блокировали операции. Открытие сокета в Python — это одна из немногих операций, освобождающих GIL, и она работает конкурентно в пуле потоков, как неблокирующая операция.

Напоминание: какие async-операции есть в asyncio?

Операция

Asyncio API

Описание

Sleep

asyncio.sleep()

Асинхронный sleep в течение заданного времени.

Потоки TCP/UDP

asyncio.open_connection()

Открывает соединение TCP/UDP.

HTTP

aiohttp.ClientSession()

Асинхронный HTTP-клиент.

Выполнение подпроцессов

asyncio.subprocess

Асинхронное выполнение подпроцессов.

Очереди

asyncio.Queue

Реализация асинхронных очередей.

Проблема 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)


  1. tumbler
    04.09.2025 12:45

    и я думаю, что в 20026 году можно начинать внедрять free-threading в каких-то узких, хорошо тестируемых сценариях.

    По Фрейду оговорочка)

    По-моему зря это они. Раньше, написав код без использования async/await, можно было надеяться, что в середину твоего `a += 1` никто не вклинится. А теперь придётся задумываться над каждой строкой, как в Go.


    1. outlingo
      04.09.2025 12:45

      Вот только если не изменяет память, то "почему-то" лучше всегда считалось не "надеяться" а писать потокобезопасно.


  1. ititkov
    04.09.2025 12:45

    Наконец-то нормальным языком, спасибо! Мне ChatGPT во время вайб-кодингового упражнения вдруг в середине начал предлагать async в тех местах, где обычно раньше реализовывал через await и не мог нормально объяснить почему и зачем.


  1. aax
    04.09.2025 12:45

    Все бы хорошо, но при преходе с версиии на версию нужные тебе библиотеки почти всегда отваливаюттся, деже если это экосистема pip.


  1. CloudlyNosound
    04.09.2025 12:45

    Асинхронщину не все понимают. Плюс, она не всем нужна. Автор оригинала замечательной статьи конечно не прочтет, но хоть тут останется.


  1. 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 — сложная задача.

    Это действительно так. Разработчики библиотек в принципе всегда страдают, так как им нужно поддерживать кучу платформ, языков, парадигм разработки. Обычные пользователи языка, коих большинство, обычно пишут либо только синхронный код, либо асинхронный с впиливанием многопоточности/многопроцессорности и не страдают от этой проблемы.

    Лично я уже слабо себе представляю разработку на питоне без асинхронности, но как уже написали выше:

    Асинхронщину не все понимают. Плюс, она не всем нужна.


  1. andreymal
    04.09.2025 12:45

    SQLAlchemy, самый популярный ORM для Python, добавил поддержку asyncio только в 2023 году

    Накостылировал через greenlet

    Мой внутренний перфекционист отказывается считать это поддержкой asyncio


  1. GamePad64
    04.09.2025 12:45

    "Ещё один всё понял"
    В Python и Rust есть проблема красно-синих функций. Асинхронный и синхронный код — два разных плохо совместимых мира. Что с этим делать и как жить — непонятно. Есть предложения различной степени удачности.


  1. homm
    04.09.2025 12:45

    Первые версии Python 3 (3.0-3.4) в основном делали упор на стабильность и упрощение перехода пользователей с версии 2.7.

    Те кто застал эти версии сейчас тихонько посмеялись.