Асинхронность и 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.
Если вы не уверены, используйте обычные def.
Примечание: Вы можете смешивать def и async def в своих функциях операций с путями, сколько нужно, и определять каждое из них с помощью самого подходящего варианта для вас. FastAPI правильно обработает их.
В любом случае, FastAPI по-прежнему будет работать асинхронно и очень быстро.
Но, следуя указанным шагам, можно получить некоторые улучшения производительности.
Технические детали
Современные версии Python поддерживают «асинхронный код», используя так называемые «генераторы», с синтаксисом async и await.
Давайте разберём эту фразу по частям в следующих разделах:
- Асинхронный код
asyncиawait- Генераторы
Асинхронный код
Асинхронный код просто означает, что язык 💬 может указать компьютеру/программе 🤖, что в какой-то момент кода ему 🤖 придётся подождать завершения чего-то другого в другом месте. Допустим, это что-то другое называется "медленный-файл" 📝.
Таким образом, в это время компьютер может заняться другой работой, пока "медленный-файл" 📝 завершается.
Затем компьютер/программа 🤖 возвращается каждый раз, когда есть возможность, так как снова ждёт, или когда он 🤖 завершил всю работу в данный момент. И он 🤖 проверяет, завершились ли какие-либо задачи, которые он ожидал, выполняя соответствующие действия.
Далее он 🤖 берёт первую завершённую задачу (например, наш "медленный-файл" 📝) и продолжает выполнение оставшейся работы с ней.
Это "ожидание чего-то другого" обычно относится к операциям Ввода-вывода, которые относительно "медленные" (по сравнению со скоростью процессора и оперативной памяти), такие как ожидание:
- получения данных от клиента через сеть
- получения данных, отправленных вашей программой, клиентом через сеть
- чтения содержимого файла на диске системой и передачи его вашей программе
- записи содержимого, предоставленного вашей программой, на диск
- удалённой операции API
- завершения операции с базой данных
- получения результатов запроса к базе данных
- и т. д.
Так как время выполнения в основном тратится на ожидание операций Ввода-вывода, их называют операциями «связанными с вводом-выводом».
Это называется «асинхронным», потому что компьютеру/программе не нужно быть «синхронизированным» с медленной задачей, ожидая точного момента завершения задачи, ничего не делая, чтобы получить результат задачи и продолжить работу.
Вместо этого, будучи «асинхронной» системой, по завершении задачи она может немного подождать (несколько микросекунд), пока компьютер/программа завершит текущую работу, а затем вернуться, чтобы получить результаты и продолжить работу с ними.
Для «синхронного» (в отличие от «асинхронного») также часто используется термин «последовательный», так как компьютер/программа выполняет все шаги последовательно, прежде чем перейти к другой задаче, даже если эти шаги включают ожидание.
Конкурентность и гамбургеры
Эта идея асинхронного кода, описанная выше, иногда также называется «конкурентностью». Она отличается от «параллельности».
Конкурентность и параллельность обе относятся к «разным вещам, происходящим более или менее одновременно».
Но детали между конкурентностью и параллельностью довольно разные.
Чтобы увидеть разницу, представьте следующую историю про гамбургеры:
Конкурентные гамбургеры
Вы идёте с возлюбленной на быструю еду, стоите в очереди, пока кассир принимает заказы от людей перед вами. 😍

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

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

Вы оплачиваете. 💸
Кассир даёт вам номер вашей очереди.

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

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

Вы и ваша возлюбленная едите гамбургеры и приятно проводите время. ✨

Информация
Красивые иллюстрации от Кетрины Томпсон. 🎨
Представьте, что вы компьютер/программа 🤖 в этой истории.
Пока вы стоите в очереди, вы просто без дела 😴, ждёте своей очереди, ничего не делаете. Но очередь движется быстро, потому что кассир только принимает заказы (не готовит), так что это нормально.
Затем, когда ваша очередь, вы выполняете реальную «продуктивную» работу: просматриваете меню, решаете, что хотите, выбираете, что хочет ваша возлюбленная, оплачиваете, проверяете, что вам выставили правильный счёт или оплатили картой, проверяете, что в заказе все элементы, и т. д.
Но даже если ваши гамбургеры ещё не готовы, ваша работа с кассиром «пауза» ⏸, потому что вам нужно подождать 🕙, пока ваши гамбургеры приготовятся.
Но когда вы уходите от стойки и садитесь за стол с возлюбленной, вы можете переключить 🔀 своё внимание на неё и «работать» ⏯ 🤓 над этим. Тогда вы снова делаете что-то «продуктивное», например, флиртуете с ней 😍.
Затем кассир 💁 говорит «я закончил с гамбургерами», выведя ваш номер на табло, но вы не бросаетесь сразу, когда номер вашей очереди появляется на табло. Вы знаете, что никто не украдёт ваши гамбургеры, потому что у вас есть номер вашей очереди, и у них свои.
Поэтому вы ждёте, пока ваша возлюбленная закончит историю (завершит текущую работу ⏯ / обрабатываемую задачу 🤓), вежливо улыбаетесь и говорите, что пойдёте за гамбургерами ⏸.
Затем вы подходите к стойке 🔀, к исходной задаче, которая теперь завершена ⏯, берёте гамбургеры, благодарите и приносите их за стол. Это завершает этот этап/задачу взаимодействия со стойкой ⏹. Это, в свою очередь, создаёт новую задачу «есть гамбургеры» 🔀 ⏯, но предыдущая «получить гамбургеры» завершена ⏹.
Параллельные гамбургеры
Теперь давайте представим, что это не «Конкурентные гамбургеры», а «Параллельные гамбургеры».
Вы идёте с возлюбленной на быструю еду, где много параллелизма.
Вы стоите в очереди, пока несколько (например, 8) кассиров-поваров одновременно принимают заказы от людей перед вами.
Все, кто были перед вами, ждут, пока их гамбургеры будут готовы, прежде чем уйти от стойки, потому что каждый из 8 кассиров сразу же готовит гамбургер, прежде чем получить следующий заказ.

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

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

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

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

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

Не было много разговоров или флирта, так как большую часть времени вы провели в ожидании 🕙 у стойки. 😞
Информация
Красивые иллюстрации от Кетрины Томпсон. 🎨
В этом сценарии с параллельными гамбургерами вы — компьютер/программа 🤖 с двумя процессорами (вы и ваша возлюбленная), оба ждущие 🕙 и уделяющие внимание ⏯ «ожиданию у стойки» 🕙 в течение длительного времени.
У заведения быстрой еды 8 процессоров (кассиров/поваров). В то время как в заведении с конкурентными гамбургерами, возможно, было только 2 (один кассир и один повар).
Но всё равно итоговый опыт не самый лучший. 😞
Это будет параллельная версия истории про гамбургеры. 🍔
Для более реального примера представьте банк.
До недавнего времени большинство банков имели нескольких кассиров 👨💼👨💼👨💼👨💼 и большую очередь 🕙🕙🕙🕙🕙🕙🕙🕙.
Все кассиры выполняли всю работу с одним клиентом за другим 👨💼⏯.
И вам нужно долго ждать 🕙 в очереди, иначе вы теряете свою очередь.
Вы, вероятно, не захотите брать с собой возлюбленную 😍, чтобы идти по делам в банк 🏦.
Заключение по гамбургерам
В этом сценарии с «гамбургерами с вашей возлюбленной», учитывая много ожидания 🕙, намного логичнее иметь конкурентную систему ⏸🔀⏯.
END_OF_DOCUMENT_MARKERВ большинстве веб-приложений ситуация такова.
Множество пользователей, но ваш сервер ожидает 🕙, пока их не очень хорошее подключение отправит запросы.
Затем снова ожидает 🕙, пока ответы не вернутся.
Это «ожидание» 🕙 измеряется в микросекундах, но всё равно, суммарно, это много ожидания в итоге.
Поэтому использование асинхронного ⏸🔀⏯ кода для веб-API имеет большой смысл.
Такая асинхронность сделала NodeJS популярным (хотя NodeJS не является параллельным), и это сила языка программирования Go.
И такую же производительность вы получите с FastAPI.
И поскольку у вас может быть и параллельность, и асинхронность одновременно, вы получите более высокую производительность, чем у большинства протестированных фреймворков NodeJS, и на уровне Go, который является компилируемым языком, близким к C (благодаря Starlette).
Лучше ли конкурентность, чем параллелизм?
Нет! Это не мораль истории.
Конкурентность отличается от параллелизма. И она лучше в конкретных сценариях, которые включают много ожидания. Из-за этого она, как правило, намного лучше, чем параллелизм, для разработки веб-приложений. Но не для всего.
Чтобы уравновесить это, представьте себе следующую короткую историю:
Вам нужно убрать большой грязный дом.
Да, это вся история.
Ожидания 🕙 нигде нет, только много работы, которая должна быть выполнена в нескольких местах дома.
Вы могли бы поочерёдно, как в примере с гамбургерами, сначала гостиную, потом кухню, но так как вы не ждёте 🕙 ничего, просто убираете и убираете, очередь ни на что не повлияет.
Это займет одинаковое количество времени с очередью (конкурентностью) или без неё, и вы выполнили бы одинаковое количество работы.
Но в этом случае, если бы у вас были 8 бывших кассиров/поваров/сейчас уборщиков, и каждый из них (плюс вы) могли бы взять зону дома для уборки, вы могли бы сделать всю работу параллельно, с дополнительной помощью и закончить намного быстрее.
В этом сценарии каждый уборщик (включая вас) был бы процессором, выполняющим свою часть работы.
И так как большая часть времени выполнения тратится на фактическую работу (а не на ожидание), а работа в компьютере выполняется процессором ЦП, эти задачи называют «привязанными к ЦП».
Общие примеры задач, привязанных к ЦП, — это задачи, которые требуют сложных математических вычислений.
Например:
- Обработка звука или обработка изображений.
- Компьютерное зрение: изображение состоит из миллионов пикселей, каждый пиксель имеет 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, нужно её «ожидать». Поэтому это не сработает:
# 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, «Goroutines».
Заключение
Посмотрим на ту же фразу, что и выше:
Современные версии Python поддерживают «асинхронный код» с использованием так называемых «корутин», с синтаксисом
asyncиawait.
Теперь это должно быть понятнее. ✨
Всё это питает FastAPI (через Starlette) и делает его производительность такой впечатляющей.
Очень технические детали
Предупреждение
Вы, вероятно, можете пропустить это.
Это очень технические детали того, как FastAPI работает изнутри.
Если у вас довольно глубокие технические знания (корутины, потоки, блокирование и т.д.) и вы хотите узнать, как FastAPI обрабатывает async def по сравнению с обычным def, добро пожаловать.
Функции операций с путями
Когда вы объявляете функцию операции с путём с обычным def вместо async def, она выполняется во внешнем пуле потоков, который затем ожидает, а не вызывается напрямую (так как это заблокирует сервер).
Если вы пришли из другой асинхронной среды, которая не работает описанным выше способом, и привыкли определять тривиальные функции обработки путей (path operation functions) только для вычислений с обычными def для небольшой выгоды в производительности (около 100 наносекунд), имейте в виду, что в FastAPI эффект будет обратным. В таких случаях лучше использовать async def, если ваши функции обработки путей (path operation functions) не содержат кода, выполняющего блокирующие операции ввода-вывода (I/O).
Тем не менее, в обеих ситуациях FastAPI, скорее всего, будет быстрее (или, по крайней мере, сопоставим по скорости) по сравнению с вашей предыдущей средой.
Зависимости
То же самое относится к зависимостям. Если зависимость — это стандартная def функция, а не async def, она выполняется во внешнем пуле потоков.
Подзависимости
У вас могут быть несколько зависимостей и подзависимостей, требующих друг друга (как параметры определений функций), некоторые из них могут быть созданы с помощью async def , а некоторые — с помощью обычных def. Это всё равно будет работать, и те, что созданы с помощью обычных def, будут вызываться во внешнем потоке (из пула потоков), а не будут «ожидать» выполнения.
Другие служебные функции
Любая другая служебная функция, которую вы вызываете напрямую, может быть создана с помощью обычных def или async def и FastAPI не повлияет на способ её вызова.
Это контрастирует с функциями, которые FastAPI вызывает за вас: функциями обработки путей (path operation functions) и зависимостями.
Если ваша служебная функция — это обычная функция с def, она будет вызвана напрямую (как вы написали в своём коде), а не в пуле потоков. Если функция создана с async def, то вам необходимо await для этой функции, когда вы вызываете её в своём коде.
Опять же, это очень технические детали, которые, вероятно, будут полезны, если вы их ищете.
В противном случае, вам должны подойти рекомендации из раздела выше: В спешке?.
© 2018 Sebastián Ramírez
Licensed under the MIT License.
https://fastapi.tiangolo.com/async/