Movatterモバイル変換


[0]ホーム

URL:


Перейти к содержанию
Join theFastAPI Cloud waiting list 🚀
Follow@fastapi onX (Twitter) to stay updated
FollowFastAPI onLinkedIn to stay updated
Subscribe to theFastAPI and friends newsletter 🎉
sponsor
sponsor
sponsor
sponsor
sponsor
sponsor
sponsor
sponsor
sponsor
sponsor
sponsor

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

🌐 Перевод выполнен с помощью ИИ и людей

Этот перевод был сделан ИИ под руководством людей. 🤝

В нем могут быть ошибки из-за неправильного понимания оригинального смысла или неестественности и т. д. 🤖

Вы можете улучшить этот перевод,помогая нам лучше направлять ИИ LLM.

Английская версия

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

Нет времени?

TL;DR:

Если вы используете сторонние библиотеки, которые нужно вызывать сawait, например:

results=awaitsome_library()

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

@app.get('/')asyncdefread_results():results=awaitsome_library()returnresults

Примечание

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


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

@app.get('/')defresults():results=some_library()returnresults

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


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


Примечание: вы можете смешиватьdef иasync def вфункциях-обработчиках пути столько, сколько нужно, и объявлять каждую так, как лучше для вашего случая. FastAPI сделает с ними всё как надо.

В любом из случаев выше FastAPI всё равно работает асинхронно и очень быстро.

Но следуя этим шагам, он сможет выполнить некоторые оптимизации производительности.

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

Современные версии Python поддерживают«асинхронный код» с помощью«сопрограмм» (coroutines) и синтаксисаasync иawait.

Разберём эту фразу по частям в разделах ниже:

  • Асинхронный код
  • async иawait
  • Сопрограммы

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

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

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

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

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

Это «ожидание чего-то ещё» обычно относится к операциямI/O, которые относительно «медленные» (по сравнению со скоростью процессора и оперативной памяти), например ожидание:

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

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

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

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

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

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

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

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

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

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

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

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

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

Вы платите. 💸

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

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

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

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

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

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

Информация

Прекрасные иллюстрации отKetrina Thompson. 🎨


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

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

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

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

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

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

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

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

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

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

Вы идёте со своей возлюбленной за параллельным фастфудом.

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

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

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

Вы платите 💸.

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

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

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

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

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

Вы берёте бургеры и идёте со своей возлюбленной к столику.

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

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

Информация

Прекрасные иллюстрации отKetrina Thompson. 🎨


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

В ресторане 8 процессоров (кассиров/поваров). Тогда как в «конкурентных бургерах» могло быть только 2 (один кассир и один повар).

И всё же финальный опыт — не самый лучший. 😞


Это была параллельная версия истории про бургеры. 🍔

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

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

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

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

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

Вывод про бургеры

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

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

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

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

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

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

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

Того же уровня производительности вы получаете сFastAPI.

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

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

Нет! Мораль истории не в этом.

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

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

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

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


Здесь нигде нет ожидания 🕙, просто очень много работы в разных местах дома.

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

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

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

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

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


Типичные примеры CPU-bound операций — те, которые требуют сложной математической обработки.

Например:

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

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

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

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

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

async иawait

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

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

burgers=awaitget_burgers(2)

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

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

asyncdefget_burgers(number:int):# Сделать что-то асинхронное, чтобы приготовить бургерыreturnburgers

...вместоdef:

# Это не асинхронный кодdefget_sequential_burgers(number:int):# Сделать что-то последовательное, чтобы приготовить бургерыreturnburgers

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

Когда вы хотите вызвать функцию, объявленную сasync def, нужно её «ожидать». Поэтому вот так не сработает:

# Это не сработает, потому что get_burgers определена с: async defburgers=get_burgers(2)

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

@app.get('/burgers')asyncdefread_burgers():burgers=awaitget_burgers(2)returnburgers

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

Вы могли заметить, чтоawait можно использовать только внутри функций, определённых сasync def.

Но при этом функции, определённые сasync def, нужно «ожидать». Значит, функции сasync def тоже можно вызывать только из функций, определённых сasync def.

Так что же с «яйцом и курицей» — как вызвать первуюasync функцию?

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

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

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

Starlette (иFastAPI) основаны наAnyIO, что делает их совместимыми и со стандартной библиотекой Pythonasyncio, и сTrio.

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

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

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

Другие формы асинхронного кода

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

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

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

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

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

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

Сопрограммы

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

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

Заключение

Вернёмся к той же фразе:

Современные версии Python поддерживают«асинхронный код» с помощью«сопрограмм» (coroutines) и синтаксисаasync иawait.

Теперь это должно звучать понятнее. ✨

Именно это «движет» FastAPI (через Starlette) и обеспечивает столь впечатляющую производительность.

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

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

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

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

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

Функции-обработчики пути

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

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

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

Зависимости

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

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

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

Другие служебные функции

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

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

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


Снова: это очень технические подробности, полезные, вероятно, только если вы целенаправленно их ищете.

Иначе вам достаточно руководствоваться рекомендациями из раздела выше:Нет времени?.


[8]ページ先頭

©2009-2026 Movatter.jp