Spec-Zone.ru › FastAPI

Конкурентность и async / await

Подробная информация о синтаксисе async def для функций операций пути, а также некоторые сведения об асинхронном коде, конкурентности и параллелизме.

Нет времени?

Кратко:

Если вы используете сторонние библиотеки, которые предлагают вызывать их с помощью await, например:

results = await some_library()

Тогда объявляйте свои функции операций пути с помощью async def, например:

@app.get('/')
async def read_results():
    results = await some_library()
    return results

Примечание

Использовать await можно только внутри функций, созданных с помощью async def.


Если вы используете стороннюю библиотеку, которая взаимодействует с чем-либо (базой данных, API, файловой системой и т. д.) и не поддерживает использование await (так обстоит дело с большинством библиотек для работы с базами данных), объявляйте свои функции операций пути как обычно, используя только def, например:

@app.get('/')
def results():
    results = some_library()
    return results

Если вашему приложению (по какой-либо причине) не нужно взаимодействовать с чем-либо ещё и ждать ответа, используйте async def, даже если внутри вам не нужно использовать await.


Если вы не уверены, используйте обычный def.


Примечание: В своих функциях операций пути вы можете сочетать def и async def в любом необходимом количестве и определять каждую функцию с наиболее подходящим для вас вариантом. FastAPI обработает их правильно.

В любом случае, FastAPI продолжит работать асинхронно и будет очень быстрым.

Но если следовать приведённым выше рекомендациям, можно добиться некоторых оптимизаций производительности.

Технические подробности

Современные версии Python поддерживают «асинхронный код» с помощью так называемых «корутин», используя синтаксис async и await.

В следующих разделах рассмотрим эту фразу по частям:

  • Асинхронный код
  • async и await
  • Корутины

Асинхронный код

Асинхронный код означает, что в языке 💬 есть способ сообщить компьютеру / программе 🤖, что в какой-то момент выполнения кода придётся подождать, пока где-то ещё завершится что-то другое. Допустим, это что-то другое называется «медленный-файл» 📝.

В это время компьютер может заняться какой-нибудь другой работой, пока «медленный-файл» 📝 завершается.

Затем компьютер / программа 🤖 будет возвращаться к этой работе всякий раз, когда появится возможность, потому что он снова чего-то ждёт, либо когда завершит всю работу, которую выполнял в тот момент. Он 🤖 проверит, завершились ли уже какие-либо задачи, которых он ждал, и выполнит необходимые действия.

Затем он 🤖 возьмёт первую завершившуюся задачу (скажем, наш «медленный-файл» 📝) и продолжит работу, которую нужно было с ней выполнить.

Ожидание «чего-то другого» обычно связано с операциями ввода-вывода, которые относительно «медленные» (по сравнению со скоростью процессора и оперативной памяти), например ожидание:

  • передачи данных от клиента по сети
  • получения клиентом по сети данных, отправленных вашей программой
  • чтения системой содержимого файла на диске и передачи его вашей программе
  • записи на диск содержимого, переданного вашей программой системе
  • выполнения удалённой операции API
  • завершения операции с базой данных
  • возвращения результатов запроса к базе данных
  • и т. д.

Поскольку большая часть времени выполнения уходит на ожидание операций ввода-вывода, их называют операциями с ограничением по вводу-выводу («I/O bound»).

Это называется «асинхронностью», потому что компьютеру / программе не нужно быть «синхронизированным» с медленной задачей и бездействовать, дожидаясь точного момента её завершения, чтобы получить результат и продолжить работу.

Вместо этого в «асинхронной» системе после завершения задача может немного подождать в очереди (несколько микросекунд), пока компьютер / программа не закончит работу, которой занялись, а затем вернуться, чтобы передать результаты и продолжить работу с ними.

Для «синхронного» выполнения (в отличие от «асинхронного») также часто используют термин «последовательное», поскольку компьютер / программа выполняет все шаги по порядку, прежде чем переключиться на другую задачу, даже если эти шаги связаны с ожиданием.

Конкурентность и бургеры

Описанную выше идею асинхронного кода также иногда называют «конкурентностью». Она отличается от «параллелизма».

И конкурентность, и параллелизм связаны с тем, что «разные вещи происходят примерно одновременно».

Но между конкурентностью и параллелизмом есть существенные различия.

Чтобы понять разницу, представьте следующую историю о бургерах:

Конкурентные бургеры

Вы идёте со своей симпатией в ресторан быстрого питания и стоите в очереди, пока кассир принимает заказы у людей перед вами. 😍

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

Кассир говорит что-то повару на кухне, чтобы тот знал, что нужно приготовить ваши бургеры (хотя сейчас он готовит бургеры для предыдущих клиентов).

Вы платите. 💸

Кассир выдаёт вам номер заказа.

Пока вы ждёте, вы идёте со своей симпатией выбирать столик, садитесь и долго разговариваете (ведь ваши бургеры очень изысканные и требуют времени на приготовление).

Сидя за столиком со своей симпатией и ожидая бургеры, вы можете провести это время, любуясь тем, какой замечательный, милый и умный человек вам нравится ✨😍✨.

Пока вы ждёте и разговариваете со своей симпатией, время от времени вы проверяете номер на табло у стойки, чтобы узнать, подошла ли ваша очередь.

И наконец, наступает ваша очередь. Вы подходите к стойке, забираете бургеры и возвращаетесь к столику.

Вы со своей симпатией едите бургеры и приятно проводите время. ✨

Примечание

Прекрасные иллюстрации созданы Кетриной Томпсон. 🎨


Представьте, что в этой истории вы — компьютер / программа 🤖.

Пока вы стоите в очереди, вы просто бездействуете 😴 и ждёте своей очереди, не занимаясь ничем особо «продуктивным». Но очередь движется быстро, ведь кассир только принимает заказы, а не готовит бургеры, так что это не проблема.

Затем, когда подходит ваша очередь, вы выполняете настоящую «продуктивную» работу: изучаете меню, решаете, чего хотите, узнаёте выбор своей симпатии, платите, проверяете, что дали нужную купюру или карту, убеждаетесь, что с вас списали правильную сумму и что в заказе указаны нужные блюда, и так далее.

Но затем, хотя у вас всё ещё нет бургеров, работа с кассиром «приостанавливается» ⏸, потому что вам нужно ждать 🕙, пока бургеры не будут готовы.

Но, отойдя от стойки и усевшись за столик с номером заказа, вы можете переключить 🔀 внимание на свою симпатию и «заняться» ⏯ 🤓 этим. И снова делать что-то очень «продуктивное» — флиртовать с человеком, который вам нравится 😍.

Затем кассир 💁 сообщает: «Бургеры готовы», показывая ваш номер на табло у стойки. Но вы не вскакиваете как сумасшедший сразу же, как только на табло появляется ваш номер. Вы знаете, что никто не заберёт ваши бургеры, потому что у вас есть номер заказа, а у остальных — свои номера.

Поэтому вы ждёте, пока ваша симпатия закончит рассказ (завершит текущую работу ⏯ / обрабатываемую задачу 🤓), мягко улыбаетесь и говорите, что идёте за бургерами ⏸.

Затем вы возвращаетесь к стойке 🔀, к исходной задаче, которая теперь завершена ⏯, забираете бургеры, благодарите и относите их к столику. На этом этап / задача взаимодействия со стойкой завершается ⏹. В свою очередь, это создаёт новую задачу — «есть бургеры» 🔀 ⏯, тогда как предыдущая задача «забрать бургеры» завершена ⏹.

Параллельные бургеры

А теперь представим, что это не «конкурентные бургеры», а «параллельные бургеры».

Вы идёте со своей симпатией в ресторан быстрого питания с параллельным обслуживанием.

Вы стоите в очереди, пока несколько (скажем, 8) кассиров, которые одновременно являются поварами, принимают заказы у людей перед вами.

Все, кто стоит перед вами, ждут готовности своих бургеров у стойки, потому что каждый из 8 кассиров сразу же идёт готовить бургер и только потом принимает следующий заказ.

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

Вы платите 💸.

Кассир идёт на кухню.

Вы ждёте 🕙, стоя перед стойкой, чтобы никто другой не забрал ваши бургеры: номеров очереди здесь нет.

Поскольку вы со своей симпатией заняты тем, что не даёте никому пройти вперёд и забрать ваши бургеры, когда они будут готовы, вы не можете уделить внимание своей симпатии. 😞

Это «синхронная» работа: вы «синхронизированы» с кассиром-поваром 👨‍🍳. Вам нужно ждать 🕙 и находиться там именно в тот момент, когда кассир-повар 👨‍🍳 закончит готовить бургеры и отдаст их вам, иначе их может забрать кто-нибудь другой.

Наконец, кассир-повар 👨‍🍳 возвращается с вашими бургерами после долгого ожидания 🕙 у стойки.

Вы забираете бургеры и идёте со своей симпатией к столику.

Вы просто съедаете их, и всё. ⏹

Вы почти не разговаривали и не флиртовали, потому что большую часть времени провели в ожидании 🕙 у стойки. 😞

Примечание

Прекрасные иллюстрации созданы Кетриной Томпсон. 🎨


В этом сценарии с параллельными бургерами вы — компьютер / программа 🤖 с двумя процессорами (вы и ваша симпатия), которые оба ждут 🕙 и уделяют внимание ⏯ длительному «ожиданию у стойки» 🕙.

В ресторане быстрого питания работают 8 процессоров (кассиров-поваров). А в ресторане с конкурентным обслуживанием их могло быть всего 2 (один кассир и один повар).

Но, несмотря на это, впечатления всё равно не самые лучшие. 😞


Так выглядела бы история о бургерах с параллельным обслуживанием. 🍔

Для более «жизненного» примера представьте себе банк.

До недавнего времени в большинстве банков было несколько кассиров 👨‍💼👨‍💼👨‍💼👨‍💼 и длинная очередь 🕙🕙🕙🕙🕙🕙🕙🕙.

Каждый кассир выполнял всю работу с одним клиентом за другим 👨‍💼⏯.

И вам приходилось долго ждать 🕙 в очереди, иначе вы теряли своё место.

Вы, вероятно, не захотели бы брать свою симпатию 😍 с собой по делам в банк 🏦.

Вывод о бургерах

В этом сценарии — «бургеры из фастфуда с человеком, который вам нравится» — приходится много ждать 🕙, поэтому гораздо разумнее использовать конкурентную систему ⏸🔀⏯.

Так обстоит дело с большинством веб-приложений.

Пользователей очень много, а сервер ждёт 🕙, пока их не слишком хорошее подключение отправит запросы.

А затем снова ждёт 🕙, пока не придут ответы.

Это ожидание 🕙 измеряется микросекундами, но в сумме ожидания всё равно получается много.

Поэтому для веб-API имеет смысл использовать асинхронный код ⏸🔀⏯.

Именно такая асинхронность сделала NodeJS популярным (хотя NodeJS не является параллельным), и в этом заключается сильная сторона языка программирования Go.

И такую же производительность вы получаете с FastAPI.

А поскольку можно одновременно использовать параллелизм и асинхронность, производительность будет выше, чем у большинства протестированных фреймворков NodeJS, и сопоставима с Go — компилируемым языком, близким к C (всё это благодаря Starlette).

Конкурентность лучше параллелизма?

Нет! В этом не заключается мораль истории.

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

Чтобы уравновесить это утверждение, представьте следующую короткую историю:

Вам нужно убрать большой грязный дом.

Да, это вся история.


Нигде не нужно ждать 🕙 — просто есть много работы в разных местах дома.

Можно убирать по очереди, как в примере с бургерами: сначала гостиную, потом кухню. Но поскольку ждать 🕙 ничего не нужно, а нужно лишь убираться, такая очерёдность ничего не изменит.

На завершение уйдёт столько же времени независимо от того, будете ли вы работать по очереди (конкурентно), и объём выполненной работы будет одинаковым.

Но в этом случае, если бы вы могли позвать 8 бывших кассиров-поваров, которые теперь стали уборщиками, и каждый из них (вместе с вами) убирал бы свою зону дома, вы смогли бы выполнить всю работу параллельно и, благодаря дополнительной помощи, закончить гораздо быстрее.

В этом сценарии каждый уборщик (включая вас) был бы процессором, выполняющим свою часть работы.

А поскольку большая часть времени выполнения уходит на саму работу, а не на ожидание, а работу в компьютере выполняет ЦП, такие задачи называют задачами с ограничением по процессору («CPU bound»).


Распространённые примеры задач с ограничением по процессору — задачи, требующие сложных математических вычислений.

Например:

  • Обработка аудио или изображений.
  • Компьютерное зрение: изображение состоит из миллионов пикселей, у каждого пикселя есть 3 значения / цвета, и обработка обычно требует одновременно выполнить какие-либо вычисления для всех этих пикселей.
  • Машинное обучение: обычно оно требует множества умножений «матриц» и «векторов». Представьте себе огромную электронную таблицу с числами, которые нужно перемножить все одновременно.
  • Глубокое обучение: это подраздел машинного обучения, поэтому здесь применимо то же самое. Только перемножать нужно не одну таблицу чисел, а огромное их множество; во многих случаях для создания и / или использования этих моделей применяется специальный процессор.

Конкурентность + параллелизм: веб + машинное обучение

С помощью FastAPI можно воспользоваться конкурентностью, которая широко применяется в веб-разработке (это же главное преимущество NodeJS).

Но можно также использовать преимущества параллелизма и многопроцессорной обработки (одновременного выполнения нескольких процессов) для задач с ограничением по процессору, таких как задачи в системах машинного обучения.

Благодаря этому, а также тому, что Python — основной язык для анализа данных, машинного обучения и особенно глубокого обучения, FastAPI отлично подходит для веб-API и приложений в сфере анализа данных / машинного обучения (а также для многих других задач).

О том, как добиться такого параллелизма в рабочей среде, см. раздел Развёртывание.

async и await

В современных версиях Python есть очень интуитивно понятный способ определять асинхронный код. Благодаря ему код выглядит почти как обычный «последовательный», а ожидание выполняется в нужные моменты автоматически.

Если операция требует ожидания перед выдачей результата и поддерживает эти новые возможности Python, её можно записать так:

burgers = await get_burgers(2)

Здесь ключевое слово — await. Оно сообщает Python, что нужно подождать ⏸, пока get_burgers(2) не завершит свою работу 🕙, прежде чем сохранить результат в burgers. Благодаря этому Python понимает, что тем временем можно заняться чем-нибудь другим 🔀 ⏯ (например, принять другой запрос).

Чтобы работало await, оно должно находиться внутри функции, поддерживающей асинхронность. Для этого достаточно объявить её с помощью async def:

async def get_burgers(number: int):
    # Do some asynchronous stuff to create the burgers
    return burgers

...вместо def:

# This is not asynchronous
def get_sequential_burgers(number: int):
    # Do some sequential stuff to create the burgers
    return burgers

Благодаря async def Python знает, что внутри этой функции нужно учитывать выражения await, а также что выполнение функции можно «приостановить» ⏸, заняться чем-нибудь другим 🔀, а затем вернуться к ней.

При вызове функции async def нужно использовать «await». Поэтому следующий код работать не будет:

# This won't work, because get_burgers was defined with: async def
burgers = get_burgers(2)

Итак, если вы используете библиотеку, которая позволяет вызывать её с помощью await, вам нужно создать функции операций пути, использующие её с помощью async def, как в примере:

@app.get('/burgers')
async def read_burgers():
    burgers = await get_burgers(2)
    return burgers

Дополнительные технические подробности

Возможно, вы заметили, что await можно использовать только внутри функций, объявленных с помощью async def.

Но при этом функции, объявленные с помощью async def, нужно «ожидать». Поэтому функции с async def можно вызывать только внутри функций, которые тоже объявлены с помощью async def.

Итак, что появляется раньше — яйцо или курица? Как вызвать первую функцию async?

Если вы работаете с FastAPI, об этом можно не беспокоиться: такой «первой» функцией будет ваша функция операции пути, и FastAPI знает, как правильно её обработать.

Но если вы хотите использовать async / await без FastAPI, это тоже возможно.

Пишите собственный асинхронный код

Starlette (и FastAPI) основан на AnyIO, благодаря чему он совместим как со стандартной библиотекой Python asyncio, так и с Trio.

В частности, вы можете напрямую использовать AnyIO для сложных сценариев конкурентности, требующих более продвинутых шаблонов в вашем коде.

Даже если вы не используете FastAPI, с помощью AnyIO можно создавать собственные асинхронные приложения, обеспечивая высокую совместимость и пользуясь его преимуществами (например, структурированной конкурентностью).

Я создал поверх AnyIO ещё одну библиотеку — тонкий дополнительный слой, который немного улучшает аннотации типов и обеспечивает более качественное автодополнение, встроенные сообщения об ошибках и т. д. В ней также есть доступное введение и руководство, которые помогут вам понять асинхронный код и писать собственный асинхронный код: Asyncer. Она особенно пригодится, если нужно сочетать асинхронный код с обычным (блокирующим / синхронным) кодом.

Другие способы написания асинхронного кода

Такой способ использования async и await появился в языке сравнительно недавно.

Но он значительно упрощает работу с асинхронным кодом.

Этот же синтаксис (или почти идентичный) недавно появился и в современных версиях JavaScript (в браузере и NodeJS).

До этого обработка асинхронного кода была гораздо сложнее.

В предыдущих версиях Python можно было использовать потоки или Gevent. Но такой код гораздо сложнее понять, отладить и осмыслить.

В предыдущих версиях NodeJS / браузерного JavaScript использовались «обратные вызовы», что приводило к «аду обратных вызовов».

Корутины

Корутина — просто очень специальный термин для объекта, возвращаемого функцией async def. Python знает, что это нечто похожее на функцию: её можно запустить, и в какой-то момент она завершится, но внутри она тоже может приостанавливать выполнение ⏸, когда встречает await.

Всю эту функциональность асинхронного кода с использованием async и await часто кратко называют использованием «корутин». Это можно сравнить с главной особенностью Go — «горрутинами».

Заключение

Вернёмся к фразе, приведённой выше:

Современные версии Python поддерживают «асинхронный код» с помощью так называемых «корутин», используя синтаксис async и await.

Теперь это должно быть понятнее. ✨

Всё это лежит в основе FastAPI (через Starlette) и обеспечивает его впечатляющую производительность.

Очень технические подробности

Предупреждение

Этот раздел, скорее всего, можно пропустить.

Здесь приведены очень технические подробности о том, как FastAPI работает под капотом.

Если у вас есть технические знания (о корутинах, потоках, блокирующих операциях и т. д.) и вам интересно, как FastAPI обрабатывает async def в сравнении с обычным def, читайте дальше.

Функции операций пути

Если объявить функцию операции пути с обычным def вместо async def, она будет выполняться в отдельном пуле потоков, а затем ожидаться, вместо того чтобы вызываться напрямую (что заблокировало бы сервер).

Если вы переходите с другого асинхронного фреймворка, который работает не так, как описано выше, и привыкли определять тривиальные функции операций пути, выполняющие только вычисления, с обычным def ради незначительного прироста производительности (около 100 наносекунд), обратите внимание: в FastAPI результат будет прямо противоположным. В таких случаях лучше использовать async def, если только ваши функции операций пути не выполняют блокирующий ввод-вывод I/O.

Тем не менее в обоих случаях FastAPI, скорее всего, всё равно будет быстрее вашего предыдущего фреймворка (или как минимум не уступит ему).

Зависимости

То же относится к зависимостям. Если зависимость — это обычная функция def, а не async def, она выполняется в отдельном пуле потоков.

Подзависимости

Можно создавать несколько зависимостей и подзависимостей, которым нужны другие зависимости (в качестве параметров определений функций); некоторые из них могут быть созданы с помощью async def, а некоторые — с помощью обычного def. Всё будет работать: функции, созданные с помощью обычного def, будут вызваны в отдельном потоке (из пула потоков), а не «ожидаться».

Другие вспомогательные функции

Любую другую вспомогательную функцию, которую вы вызываете напрямую, можно создать с помощью обычного def или async def — FastAPI не повлияет на способ её вызова.

Это отличается от функций, вызываемых FastAPI за вас: функций операций пути и зависимостей.

Если ваша вспомогательная функция — обычная функция с def, она будет вызвана напрямую (так, как вы написали её в коде), а не в пуле потоков. Если функция создана с помощью async def, при вызове в коде её нужно await.


Повторим: это очень технические подробности, которые, вероятно, пригодятся тем, кто искал именно такую информацию.

В противном случае достаточно рекомендаций из раздела выше: Нет времени?.

© 2018 Sebastián Ramírez
Licensed under the MIT License.
https://fastapi.tiangolo.com/async/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API