40 KiB
Конкурентность и async / await
Подробная информация о синтаксисе async def
для функций-обработчиков пути, а также некоторые сведения об асинхронном программировании, конкурентности и параллелизме.
Нет времени?
TL;DR:
Если вы используете сторонние библиотеки, которые требуют вызова с await
, например:
results = await some_library()
Тогда объявите ваши функции-обработчики пути с async def
, как:
@app.get('/')
async def read_results():
results = await some_library()
return results
/// note | Примечание
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
- Сопрограммы
Асинхронный код
Асинхронный код просто означает, что в языке 💬 есть способ указать компьютеру / программе 🤖, что в определённый момент кода, он 🤖 должен будет дождаться, пока что-то ещё завершится где-то ещё. Допустим, это что-то ещё называется "медленный файл" 📝.
Итак, в это время компьютер может заниматься чем-то другим, пока "медленный файл" 📝 завершает работу.
Затем компьютер / программа 🤖 возвращается каждый раз, когда у него появляется возможность, потому что он снова что-то ожидает, или вся выполняемая работа на данный момент завершена. Он 🤖 проверяет, не завершена ли какая-нибудь из ожидаемых задач, и выполняет необходимые действия.
Далее 🤖 берёт первую завершённую задачу (допустим наш "медленный файл" 📝) и продолжает действия, которые он должен был выполнить с ней.
То "ожидание чего-то ещё", как правило, относится к операциям I/O, которые относительно "медленны" (по сравнению со скоростью процессора и оперативной памяти), например:
- данные от клиента по сети
- данные, отправленные вашей программой клиенту через сеть
- содержимое файла на диске передано системе и дано вашей программе
- содержимое вашей программы передано системе для записи на диск
- удалённая операция API
- операция с базой данных завершается
- выполнение запроса к базе данных возвращает результаты
- и т. д.
Поскольку в основном время выполнения тратится на ожидание операций I/O, это называют "операциями, ограниченными скоростью ввода-вывода".
Они называются "асинхронными", поскольку компьютеру / программе не нужно быть "синхронизированными" с медленной задачей и сидеть в ожидании момента её завершения, чтобы забрать результаты и продолжить работу.
Вместо этого, в "асинхронной" системе задача, оказавшись завершённой, может немного подождать в очереди (некоторые микросекунды), пока компьютер / программа завершит то, что выполнял, и затем вернуться, чтобы забрать результаты и продолжить работу с ними.
Для "синхронного" (в противоположность "асинхронному") они часто также используют термин "последовательный", потому что компьютер / программа следует всем шагам в последовательности, прежде чем переключиться на другую задачу, даже если эти шаги включают ожидание.
Конкурентность и бургеры
Эта концепция асинхронного кода, описанного выше, также иногда называется "конкурентностью". Она отличается от "параллелизма".
Конкурентность и параллелизм оба связаны с "разными вещами, происходящими более-менее одновременно".
Но детали между конкурентностью и параллелизмом весьма различны.
Чтобы увидеть разницу, представьте следующую историю про бургеры:
Конкурентные бургеры
Вы идёте со своей возлюбленной 😍 в фастфуд, становитесь в очередь, в это время кассир принимает заказы у посетителей перед вами.

Когда, наконец, подходит очередь, вы заказываете парочку самых вкусных и навороченных бургеров 🍔🍔.

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

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

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

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

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

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

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

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

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

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

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

Разговоров и флирта было немного, поскольку большую часть времени пришлось провести у прилавка. 😞
/// info | Информация
Красивые иллюстрации от Кетрины Томпсон. 🎨
///
В этой истории о параллельных бургерах вы компьютер / программа 🤖 с двумя "процессорами" (вы и ваша возлюбленная 😍), оба ждут 🕙 и уделяют все своё внимание ⏯, чтобы стоять "на кассе" долгое время.
Фастфуд имеет 8 "процессоров" (кассиров/поваров). В то время как в магазине конкурентных бургеров, возможно, их было всего 2 (один кассир и один повар).
Но все равно итоговый опыт не из лучших. 😞
Это была бы параллельная история для бургеров. 🍔
Для более "реалистичного" примера этого представьте себе банк.
До недавнего времени в большинстве банков было несколько кассиров 👨💼👨💼👨💼👨💼 и длинные очереди 🕙🕙🕙🕙🕙🕙🕙🕙.
Кассиры выполняют всю работу для клиента один за другим 👨💼⏯.
А вам приходится долго стоять в очереди 🕙 или вы пропустите свою очередь.
Вы вряд ли хотели бы взять свою возлюбленную 😍 в банк 🏦 оплачивать налоги.
Выводы о бургерах
В этой истории с "фастфудом и бургером с вашей возлюбленной", так как много времени тратится на ожидание 🕙, имеет смысл иметь конкурентную систему ⏸🔀⏯.
Это случай для большинства веб-приложений.
Много-много пользователей, но ваш сервер ждёт 🕙 их не самого лучшего соединения, чтобы отправить их запросы.
И ждёт снова 🕙, пока их ответы вернутся.
Это "ожидание" 🕙 измеряется в микросекундах, но всё равно, складывая, это в итоге много времени ожидания.
Вот почему так важно использовать асинхронный код для веб-API.
Такая асинхронность сделала NodeJS популярным (несмотря на то, что NodeJS не параллелен) и в этом сила Go как языка программирования.
И это тот же уровень производительности, который вы получаете с FastAPI.
И, поскольку вы можете использовать параллелизм и асинхронность вместе, вы достигнете большей производительности, чем у большинства протестированных фреймворков NodeJS и на уровне Go, который является компилируемым языком ближе к C (всё благодаря Starlette).
Получается, конкурентность лучше параллелизма?
Нет. Это не мораль истории.
Конкурентность — это не то же самое, что параллелизм. Она лучше в конкретных сценариях, где много времени тратится на ожидание. Именно поэтому конкурентность часто лучше параллелизма при разработке веб-приложений. Но не всегда.
Чтобы уравновесить это, представьте следующую короткую историю:
Вам нужно убрать в большом грязном доме.
Да, это вся история.
Здесь нигде нет нужды ждать 🕙, просто много работы, которая должна быть выполнена в нескольких местах дома.
Вы могли бы устанавливать очереди, как в примере с бургерами, сначала гостиная, потом кухня, но, так как вы нигде не ждете 🕙, просто убираете и убираете, очереди не повлияют ни на что.
Это заняло бы одинаковое количество времени, чтобы завершить, с или без очередей (конкурентности), и вы сделали бы одинаковое количество работы.
Но в этом случае, если бы вы могли пригласить 8 бывших кассиров/поваров, а ныне уборщиков, и каждый из них (вместе с вами) мог бы взять участок дома, чтобы убраться, вы могли бы сделать всю работу параллельно, с дополнительной помощью, и закончить намного быстрее.
В этом сценарии каждый из уборщиков (включая вас) был бы процессором, выполняющим свою часть работы.
И поскольку большая часть времени выполнения тратится на настоящую работу (а не на ожидание), и работа в компьютере выполняется ЦП, они называют такие задачи "ограниченными производительностью процессора".
Операции, ограниченные производительностью процессора, включают в себя операции, требующие сложных математических вычислений.
Например:
- Обработка звука или изображений.
- Компьютерное зрение: изображение состоит из миллионов пикселей, каждый пиксель имеет 3 значения / цвета, и обработка требует выполнения вычислений для всех этих пикселей одновременно.
- Машинное обучение: обычно требуются множества умножений "матриц" и "векторов". Представьте большую электронную таблицу с числами и перемножьте их все сразу.
- Глубинное обучение: это подполе машинного обучения, поэтому то же самое применяется. Разница лишь в том, что у вас нет единственной электронной таблицы чисел для умножения, а есть их большое множество, и во многих случаях используется специальный процессор для создания и / atau использования таких моделей.
Конкурентность + Параллелизм: Веб + Машинное обучение
С FastAPI вы можете использовать возможности конкуретного программирования, что очень распространено для веб-разработки (именно это привлекательно в NodeJS).
Тем не менее, вы можете также использовать преимущества параллелизма и многопроцессорности (несколько процессов, работающих параллельно) для рабочих нагрузок, ограниченных по процессору, таких как в Машинное обучение.
Это, плюс простой факт, что Python является главным языком в области науки о данных, Машинного и, особенно, Глубинного обучения, делает FastAPI отличным выбором для создания веб-API и приложений для науки о данных и / atau машинного обучения (среди многих других).
Чтобы узнать, как добиться такого параллелизма в производственной эксплуатации, смотрите раздел Развертывание{.internal-link target=_blank}.
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):
# Готовим бургеры по специальному асинхронному рецепту
return burgers
...вместо def
:
# Это не асинхронный код
def get_sequential_burgers(number: int):
# Готовим бургеры последовательно по шагам
return burgers
Объявляя async def
, интерпретатор Python знает, что внутри этой функции нужно быть внимательным к выражениям await
, и что он может "поставить на паузу" ⏸ выполнение этой функции и выполнить что-то ещё 🔀 прежде, чем вернуться.
Когда вы хотите вызвать функцию с async def
, вам нужно "ожидать" её с помощью await
. Таким образом, это не сработает:
# Это не сработает, потому что get_burgers была объявлена с: 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
, должны быть "ожидание" с помощью await
. Так что функции с async def
можно вызывать только внутри функций, которые тоже определены с async def
.
Так как же тогда появилась первая курица? То есть... как вызвать первую асинхронную функцию?
Если вы работаете с FastAPI, то не нужно беспокоиться об этом, потому что эта "первая" функция будет вашей и функции-обработчиком пути, и FastAPI знает, как сделать всё правильно.
Тем не менее, если вы захотите использовать async
/ await
без FastAPI, вы также можете это сделать.
Пишите свой асинхронный код
Starlette и FastAPI основаны на AnyIO, что делает их совместимыми как со стандартной библиотекой Python's asyncio, так и с Trio.
В частности, вы можете использовать AnyIO непосредственно для ваших расширенных вариантов использования конкурентности, которые требуют более сложных шаблонов в вашем коде.
И даже если вы не использовали FastAPI, вы также можете писать свои асинхронные приложения с использованием AnyIO для обеспечения их высокой совместимости и получения его преимуществ (например, структурированной конкурентности).
Я создал ещё одну библиотеку на базе AnyIO, как тонкий слой сверху, чтобы немного улучшить аннотации типов и получить лучшее автозавершение кода, встроенные ошибки и т. д. У неё также есть дружественное введение и руководство, чтобы помочь вам понять и написать свой асинхронный код: Asyncer. Она будет особенно полезна, если вам нужно объединить асинхронный код с обычным (блокирующимся/синхронным) кодом.
Другие формы асинхронного программирования
Этот стиль использования async
и await
относительно новый в языке.
Но он сильно облегчает работу с асинхронным кодом.
Точно такой же синтаксис (или почти идентичный) недавно был включён в современные версии JavaScript (в браузере и NodeJS).
Но до этого управление асинхронным кодом было гораздо более сложным.
В предыдущих версиях Python вы могли использовать потоки или Gevent. Но код при этом становился намного сложнее для понимания, отладки и мышления.
В предыдущих версиях NodeJS/Browser JavaScript вы могли использовать "обратные вызовы". Что приводило к callback hell.
Сопрограммы
Сопрограмма - это просто высокопарный термин для обозначения той вещи, которую функция async def
возвращает. Интерпретатор Python знает, что это нечто вроде функции, что она может быть запущена и завершена в какой-то момент, но она также может быть поставлена на паузу ⏸, даже когда внутри неё есть await
.
Но вся эта функциональность использования асинхронного кода с async
и await
во многих случаях обобщается как использование "сопрограмм". Это сопоставимо с ключевой отличительной особенностью языка Go, "горутины".
Заключение
Давайте ещё раз вспомним ту фразу изначально:
Современные версии Python поддерживают "асинхронный код", используя что-то под названием "сопрограммы" с синтаксисом
async
иawait
.
Теперь это должно звучать понятнее. ✨
Именно это в основном работает в FastAPI (через Starlette) и придаёт ему такую впечатляющую производительность.
Очень технические подробности
/// warning | Предупреждение
Вы, вероятно, можете пропустить это.
Это очень технические детали внутреннего устройства FastAPI.
Если у вас действительно много технических знаний (сопрограммы, потоки, блокировка и т. д.) и вам интересно, как FastAPI обрабатывает async def
в отличие от обычного def
, читайте дальше.
///
Функции-обработчики пути
Когда вы объявляете функцию-обработчик пути стандартным способом с def
вместо async def
, она выполняется в внешнем пуле потоков, который затем ожидается, а не вызывается напрямую (это бы заблокировало сервер).
Если вы пришли из другого асинхронного фреймворка, который не работает так, как описано выше, и вы привыкли определять простые вычислительные функции с помощью обычного def
ради микроскопического прироста производительности (порядка 100 наносекунд), обратите внимание, что в FastAPI эффект будет совершенно противоположный. В этих случаях лучше использовать async def
, если только ваши функции-обработчики пути не используют код, который выполняет блокирующий I/O.
Тем не менее, несмотря на это, велика вероятность, что FastAPI будет быстрее{.internal-link target=_blank} (или по крайней мере сравнимо с) вашим предыдущим фреймворком
Зависимости
То же самое применимо и к зависимостям{.internal-link target=_blank}. Если зависимость оформлена как стандартная def
функция вместо async def
, она выполняется в внешнем пуле потоков.
Подзависимости
Вы можете иметь несколько зависимостей и подзависимостей{.internal-link target=_blank} требующих друг друга (в виде параметров определения функции), некоторые из которых могут быть созданы с помощью async def
, а некоторые с обычным def
. Они всё равно будут работать корректно, и те, которые созданы со стандартным def
, будут вызываться на внешнем потоке (из пула), а не будут "ожидать".
Другие служебные функции
Любые другие вспомогательные функции, которые вы вызываете напрямую, можно создать с помощью обычного def
или async def
, и FastAPI не будет влиять на способ их вызова.
Это контраст с функциями, которые вызываются FastAPI для вас: функции-обработчики пути и зависимости.
Если ваша служебная функция является обычной функцией с def
, она будет вызвана напрямую (как вы напишете её в коде), не в пуле потоков, если функция создана с async def
, тогда вы должны использовать await
, вызывая её в вашем коде.
Ещё раз, эти технические подробности, вероятно, будут полезны, только если вы специально их искали.
В противном случае, ознакомьтесь со сводкой из раздела выше: Нет времени?.