From e0962d0b54e401287ba69e6d1570b9b7ea302e33 Mon Sep 17 00:00:00 2001 From: Makarov Andrey Date: Mon, 9 May 2022 21:30:19 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20translation=20fo?= =?UTF-8?q?r=20`docs/async.md`=20(#4036)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/ru/docs/async.md | 505 ++++++++++++++++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 506 insertions(+) create mode 100644 docs/ru/docs/async.md diff --git a/docs/ru/docs/async.md b/docs/ru/docs/async.md new file mode 100644 index 000000000..fc5e44471 --- /dev/null +++ b/docs/ru/docs/async.md @@ -0,0 +1,505 @@ +# Конкурентность и async / await + +Здесь приведена подробная информация об использовании синтаксиса `async def` при написании *функций обработки пути*, а также рассмотрены основы асинхронного программирования, конкурентности и параллелизма. + +## Нет времени? + +TL;DR: + +Допустим, вы используете сторонюю библиотеку, которая требует вызова с ключевым словом `await`: + +```Python +results = await some_library() +``` + +В этом случае *функции обработки пути* необходимо объявлять с использованием синтаксиса `async def`: + +```Python hl_lines="2" +@app.get('/') +async def read_results(): + results = await some_library() + return results +``` + +!!! note + `await` можно использовать только внутри функций, объявленных с использованием `async def`. + +--- + +Если вы обращаетесь к сторонней библиотеке, которая с чем-то взаимодействует +(с базой данных, API, файловой системой и т. д.), и не имеет поддержки синтаксиса `await` +(что относится сейчас к большинству библиотек для работы с базами данных), то +объявляйте *функции обработки пути* обычным образом с помощью `def`, например: + +```Python hl_lines="2" +@app.get('/') +def results(): + results = some_library() + return results +``` + +--- + +Если вашему приложению (странным образом) не нужно ни с чем взаимодействовать и, соответственно, +ожидать ответа, используйте `async def`. + +--- + +Если вы не уверены, используйте обычный синтаксис `def`. + +--- + +**Примечание**: при необходимости можно смешивать `def` и `async def` в *функциях обработки пути* +и использовать в каждом случае наиболее подходящий синтаксис. А FastAPI сделает с этим всё, что нужно. + +В любом из описанных случаев FastAPI работает асинхронно и очень быстро. + +Однако придерживаясь указанных советов, можно получить дополнительную оптимизацию производительности. + +## Технические подробности + +Современные версии Python поддерживают разработку так называемого **"асинхронного кода"** посредством написания **"сопрограмм"** с использованием синтаксиса **`async` и `await`**. + +Ниже разберём эту фразу по частям: + +* **Асинхронный код** +* **`async` и `await`** +* **Сопрограммы** + +## Асинхронный код + +Асинхронный код означает, что в языке 💬 есть возможность сообщить машине / программе 🤖, +что в определённой точке кода ей 🤖 нужно будет ожидать завершения выполнения *чего-то ещё* в другом месте. Допустим это *что-то ещё* называется "медленный файл" 📝. + +И пока мы ждём завершения работы с "медленным файлом" 📝, компьютер может переключиться для выполнения других задач. + +Но при каждой возможности компьютер / программа 🤖 будет возвращаться обратно. Например, если он 🤖 опять окажется в режиме ожидания, или когда закончит всю работу. В этом случае компьютер 🤖 проверяет, не завершена ли какая-нибудь из текущих задач. + +Потом он 🤖 берёт первую выполненную задачу (допустим, наш "медленный файл" 📝) и продолжает работу, производя с ней необходимые действия. + +Вышеупомянутое "что-то ещё", завершения которого приходится ожидать, обычно относится к достаточно "медленным" операциям I/O (по сравнению со скоростью работы процессора и оперативной памяти), например: + +* отправка данных от клиента по сети +* получение клиентом данных, отправленных вашей программой по сети +* чтение системой содержимого файла с диска и передача этих данных программе +* запись на диск данных, которые программа передала системе +* обращение к удалённому API +* ожидание завершения операции с базой данных +* получение результатов запроса к базе данных +* и т. д. + +Поскольку в основном время тратится на ожидание выполнения операций I/O, +их обычно называют операциями, ограниченными скоростью ввода-вывода. + +Код называют "асинхронным", потому что компьютеру / программе не требуется "синхронизироваться" с медленной задачей и, +будучи в простое, ожидать момента её завершения, с тем чтобы забрать результат и продолжить работу. + +Вместо этого в "асинхронной" системе завершённая задача может немного подождать (буквально несколько микросекунд), +пока компьютер / программа занимается другими важными вещами, с тем чтобы потом вернуться, +забрать результаты выполнения и начать их обрабатывать. + +"Синхронное" исполнение (в противовес "асинхронному") также называют "последовательным", +потому что компьютер / программа последовательно выполняет все требуемые шаги перед тем, как перейти к следующей задаче, +даже если в процессе приходится ждать. + +### Конкурентность и бургеры + +Тот **асинхронный** код, о котором идёт речь выше, иногда называют **"конкурентностью"**. Она отличается от **"параллелизма"**. + +Да, **конкурентность** и **параллелизм** подразумевают, что разные вещи происходят примерно в одно время. + +Но внутреннее устройство **конкурентности** и **параллелизма** довольно разное. + +Чтобы это понять, представьте такую картину: + +### Конкурентные бургеры + + + +Вы идёте со своей возлюбленной 😍 в фастфуд 🍔 и становитесь в очередь, в это время кассир 💁 принимает заказы у посетителей перед вами. + +Когда наконец подходит очередь, вы заказываете парочку самых вкусных и навороченных бургеров 🍔, один для своей возлюбленной 😍, а другой себе. + +Отдаёте деньги 💸. + +Кассир 💁 что-то говорит поварам на кухне 👨‍🍳, теперь они знают, какие бургеры нужно будет приготовить 🍔 +(но пока они заняты бургерами предыдущих клиентов). + +Кассир 💁 отдаёт вам чек с номером заказа. + +В ожидании еды вы идёте со своей возлюбленной 😍 выбрать столик, садитесь и довольно продолжительное время общаетесь 😍 +(поскольку ваши бургеры самые навороченные, готовятся они не так быстро ✨🍔✨). + +Сидя за столиком с возлюбленной 😍 в ожидании бургеров 🍔, вы отлично проводите время, +восхищаясь её великолепием, красотой и умом ✨😍✨. + +Всё ещё ожидая заказ и болтая со своей возлюбленной 😍, время от времени вы проверяете, +какой номер горит над прилавком, и не подошла ли уже ваша очередь. + +И вот наконец настаёт этот момент, и вы идёте к стойке, чтобы забрать бургеры 🍔 и вернуться за столик. + +Вы со своей возлюбленной 😍 едите бургеры 🍔 и отлично проводите время ✨. + +--- + +А теперь представьте, что в этой небольшой истории вы компьютер / программа 🤖. + +В очереди вы просто глазеете по сторонам 😴, ждёте и ничего особо "продуктивного" не делаете. +Но очередь движется довольно быстро, поскольку кассир 💁 только принимает заказы (а не занимается приготовлением еды), так что ничего страшного. + +Когда подходит очередь вы наконец предпринимаете "продуктивные" действия 🤓: просматриваете меню, выбираете в нём что-то, узнаёте, что хочет ваша возлюбленная 😍, собираетесь оплатить 💸, смотрите, какую достали карту, проверяете, чтобы с вас списали верную сумму, и что в заказе всё верно и т. д. + +И хотя вы всё ещё не получили бургеры 🍔, ваша работа с кассиром 💁 ставится "на паузу" ⏸, +поскольку теперь нужно ждать 🕙, когда заказ приготовят. + +Но отойдя с номерком от прилавка, вы садитесь за столик и можете переключить 🔀 внимание +на свою возлюбленную 😍 и "работать" ⏯ 🤓 уже над этим. И вот вы снова очень +"продуктивны" 🤓, мило болтаете вдвоём и всё такое 😍. + +В какой-то момент кассир 💁 поместит на табло ваш номер, подразумевая, что бургеры готовы 🍔, но вы не станете подскакивать как умалишённый, лишь только увидев на экране свою очередь. Вы уверены, что ваши бургеры 🍔 никто не утащит, ведь у вас свой номерок, а у других свой. + +Поэтому вы подождёте, пока возлюбленная 😍 закончит рассказывать историю (закончите текущую работу ⏯ / задачу в обработке 🤓), +и мило улыбнувшись, скажете, что идёте забирать заказ ⏸. + +И вот вы подходите к стойке 🔀, к первоначальной задаче, которая уже завершена ⏯, берёте бургеры 🍔, говорите спасибо и относите заказ за столик. На этом заканчивается этап / задача взаимодействия с кассой ⏹. +В свою очередь порождается задача "поедание бургеров" 🔀 ⏯, но предыдущая ("получение бургеров") завершена ⏹. + +### Параллельные бургеры + +Теперь представим, что вместо бургерной "Конкурентные бургеры" вы решили сходить в "Параллельные бургеры". + +И вот вы идёте со своей возлюбленной 😍 отведать параллельного фастфуда 🍔. + +Вы становитесь в очередь пока несколько (пусть будет 8) кассиров, которые по совместительству ещё и повары 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳, принимают заказы у посетителей перед вами. + +При этом клиенты не отходят от стойки и ждут 🕙 получения еды, поскольку каждый +из 8 кассиров идёт на кухню готовить бургеры 🍔, а только потом принимает следующий заказ. + +Наконец настаёт ваша очередь, и вы просите два самых навороченных бургера 🍔, один для дамы сердца 😍, а другой себе. + +Ни о чём не жалея, расплачиваетесь 💸. + +И кассир уходит на кухню 👨‍🍳. + +Вам приходится ждать перед стойкой 🕙, чтобы никто по случайности не забрал ваши бургеры 🍔, ведь никаких номерков у вас нет. + +Поскольку вы с возлюбленной 😍 хотите получить заказ вовремя 🕙, и следите за тем, чтобы никто не вклинился в очередь, +у вас не получается уделять должного внимание своей даме сердца 😞. + +Это "синхронная" работа, вы "синхронизированы" с кассиром/поваром 👨‍🍳. Приходится ждать 🕙 у стойки, +когда кассир/повар 👨‍🍳 закончит делать бургеры 🍔 и вручит вам заказ, иначе его случайно может забрать кто-то другой. + +Наконец кассир/повар 👨‍🍳 возвращается с бургерами 🍔 после невыносимо долгого ожидания 🕙 за стойкой. + +Вы скорее забираете заказ 🍔 и идёте с возлюбленной 😍 за столик. + +Там вы просто едите эти бургеры, и на этом всё 🍔 ⏹. + +Вам не особо удалось пообщаться, потому что большую часть времени 🕙 пришлось провести у кассы 😞. + +--- + +В описанном сценарии вы компьютер / программа 🤖 с двумя исполнителями (вы и ваша возлюбленная 😍), +на протяжении долгого времени 🕙 вы оба уделяете всё внимание ⏯ задаче "ждать на кассе". + +В этом ресторане быстрого питания 8 исполнителей (кассиров/поваров) 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳. +Хотя в бургерной конкурентного типа было всего два (один кассир и один повар) 💁 👨‍🍳. + +Несмотря на обилие работников, опыт в итоге получился не из лучших 😞. + +--- + +Так бы выглядел аналог истории про бургерную 🍔 в "параллельном" мире. + +Вот более реалистичный пример. Представьте себе банк. + +До недавних пор в большинстве банков было несколько кассиров 👨‍💼👨‍💼👨‍💼👨‍💼 и длинные очереди 🕙🕙🕙🕙🕙🕙🕙🕙. + +Каждый кассир обслуживал одного клиента, потом следующего 👨‍💼⏯. + +Нужно было долгое время 🕙 стоять перед окошком вместе со всеми, иначе пропустишь свою очередь. + +Сомневаюсь, что у вас бы возникло желание прийти с возлюбленной 😍 в банк 🏦 оплачивать налоги. + +### Выводы о бургерах + +В нашей истории про поход в фастфуд за бургерами приходится много ждать 🕙, +поэтому имеет смысл организовать конкурентную систему ⏸🔀⏯. + +И то же самое с большинством веб-приложений. + +Пользователей очень много, но ваш сервер всё равно вынужден ждать 🕙 запросы по их слабому интернет-соединению. + +Потом снова ждать 🕙, пока вернётся ответ. + + +Это ожидание 🕙 измеряется микросекундами, но если всё сложить, то набегает довольно много времени. + +Вот почему есть смысл использовать асинхронное ⏸🔀⏯ программирование при построении веб-API. + +Большинство популярных фреймворков (включая Flask и Django) создавались +до появления в Python новых возможностей асинхронного программирования. Поэтому +их можно разворачивать с поддержкой параллельного исполнения или асинхронного +программирования старого типа, которое не настолько эффективно. + +При том, что основная спецификация асинхронного взаимодействия Python с веб-сервером +(ASGI) +была разработана командой Django для внедрения поддержки веб-сокетов. + +Именно асинхронность сделала NodeJS таким популярным (несмотря на то, что он не параллельный), +и в этом преимущество Go как языка программирования. + +И тот же уровень производительности даёт **FastAPI**. + +Поскольку можно использовать преимущества параллелизма и асинхронности вместе, +вы получаете производительность лучше, чем у большинства протестированных NodeJS фреймворков +и на уровне с Go, который является компилируемым языком близким к C (всё благодаря Starlette). + +### Получается, конкурентность лучше параллелизма? + +Нет! Мораль истории совсем не в этом. + +Конкурентность отличается от параллелизма. Она лучше в **конкретных** случаях, где много времени приходится на ожидание. +Вот почему она зачастую лучше параллелизма при разработке веб-приложений. Но это не значит, что конкурентность лучше в любых сценариях. + +Давайте посмотрим с другой стороны, представьте такую картину: + +> Вам нужно убраться в большом грязном доме. + +*Да, это вся история*. + +--- + +Тут не нужно нигде ждать 🕙, просто есть куча работы в разных частях дома. + +Можно организовать очередь как в примере с бургерами, сначала гостиная, потом кухня, +но это ни на что не повлияет, поскольку вы нигде не ждёте 🕙, а просто трёте да моете. + +И понадобится одинаковое количество времени с очередью (конкурентностью) и без неё, +и работы будет сделано тоже одинаковое количество. + +Однако в случае, если бы вы могли привести 8 бывших кассиров/поваров, а ныне уборщиков 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳, +и каждый из них (вместе с вами) взялся бы за свой участок дома, +с такой помощью вы бы закончили намного быстрее, делая всю работу **параллельно**. + +В описанном сценарии каждый уборщик (включая вас) был бы исполнителем, занятым на своём участке работы. + +И поскольку большую часть времени выполнения занимает реальная работа (а не ожидание), +а работу в компьютере делает ЦП, +такие задачи называют ограниченными производительностью процессора. + +--- + +Ограничение по процессору проявляется в операциях, где требуется выполнять сложные математические вычисления. + +Например: + +* Обработка **звука** или **изображений**. +* **Компьютерное зрение**: изображение состоит из миллионов пикселей, в каждом пикселе 3 составляющих цвета, +обработка обычно требует проведения расчётов по всем пикселям сразу. +* **Машинное обучение**: здесь обычно требуется умножение "матриц" и "векторов". +Представьте гигантскую таблицу с числами в Экселе, и все их надо одновременно перемножить. +* **Глубокое обучение**: это область *машинного обучения*, поэтому сюда подходит то же описание. +Просто у вас будет не одна таблица в Экселе, а множество. В ряде случаев используется +специальный процессор для создания и / или использования построенных таким образом моделей. + +### Конкурентность + параллелизм: Веб + машинное обучение + +**FastAPI** предоставляет возможности конкуретного программирования, +которое очень распространено в веб-разработке (именно этим славится NodeJS). + +Кроме того вы сможете использовать все преимущества параллелизма и +многопроцессорности (когда несколько процессов работают параллельно), +если рабочая нагрузка предполагает **ограничение по процессору**, +как, например, в системах машинного обучения. + +Необходимо также отметить, что Python является главным языком в области +**дата-сайенс**, +машинного обучения и, особенно, глубокого обучения. Всё это делает FastAPI +отличным вариантом (среди многих других) для разработки веб-API и приложений +в области дата-сайенс / машинного обучения. + +Как добиться такого параллелизма в эксплуатации описано в разделе [Развёртывание](deployment/index.md){.internal-link target=_blank}. + +## `async` и `await` + +В современных версиях Python разработка асинхронного кода реализована очень интуитивно. +Он выглядит как обычный "последовательный" код и самостоятельно выполняет "ожидание", когда это необходимо. + +Если некая операция требует ожидания перед тем, как вернуть результат, и +поддерживает современные возможности Python, код можно написать следующим образом: + +```Python +burgers = await get_burgers(2) +``` + +Главное здесь слово `await`. Оно сообщает интерпретатору, что необходимо дождаться ⏸ +пока `get_burgers(2)` закончит свои дела 🕙, и только после этого сохранить результат в `burgers`. +Зная это, Python может пока переключиться на выполнение других задач 🔀 ⏯ +(например получение следующего запроса). + +Чтобы ключевое слово `await` сработало, оно должно находиться внутри функции, +которая поддерживает асинхронность. Для этого вам просто нужно объявить её как `async def`: + +```Python hl_lines="1" +async def get_burgers(number: int): + # Готовим бургеры по специальному асинхронному рецепту + return burgers +``` + +...вместо `def`: + +```Python hl_lines="2" +# Это не асинхронный код +def get_sequential_burgers(number: int): + # Готовим бургеры последовательно по шагам + return burgers +``` + +Объявление `async def` указывает интерпретатору, что внутри этой функции +следует ожидать выражений `await`, и что можно поставить выполнение такой функции на "паузу" ⏸ и +переключиться на другие задачи 🔀, с тем чтобы вернуться сюда позже. + +Если вы хотите вызвать функцию с `async def`, вам нужно "ожидать" её. +Поэтому такое не сработает: + +```Python +# Это не заработает, поскольку get_burgers объявлена с использованием async def +burgers = get_burgers(2) +``` + +--- + +Если сторонняя библиотека требует вызывать её с ключевым словом `await`, +необходимо писать *функции обработки пути* с использованием `async def`, например: + +```Python hl_lines="2-3" +@app.get('/burgers') +async def read_burgers(): + burgers = await get_burgers(2) + return burgers +``` + +### Технические подробности + +Как вы могли заметить, `await` может применяться только в функциях, объявленных с использованием `async def`. + + +Но выполнение такой функции необходимо "ожидать" с помощью `await`. +Это означает, что её можно вызвать только из другой функции, которая тоже объявлена с `async def`. + +Но как же тогда появилась первая курица? В смысле... как нам вызвать первую асинхронную функцию? + +При работе с **FastAPI** просто не думайте об этом, потому что "первой" функцией является ваша *функция обработки пути*, +и дальше с этим разберётся FastAPI. + +Кроме того, если хотите, вы можете использовать синтаксис `async` / `await` и без FastAPI. + +### Пишите свой асинхронный код + +Starlette (и **FastAPI**) основаны на AnyIO, что делает их совместимыми как со стандартной библиотекой asyncio в Python, так и с Trio. + +В частности, вы можете напрямую использовать AnyIO в тех проектах, где требуется более сложная логика работы с конкурентностью. + +Даже если вы не используете FastAPI, вы можете писать асинхронные приложения с помощью AnyIO, чтобы они были максимально совместимыми и получали его преимущества (например *структурную конкурентность*). + +### Другие виды асинхронного программирования + +Стиль написания кода с `async` и `await` появился в языке Python относительно недавно. + +Но он сильно облегчает работу с асинхронным кодом. + +Ровно такой же синтаксис (ну или почти такой же) недавно был включён в современные версии JavaScript (в браузере и NodeJS). + +До этого поддержка асинхронного кода была реализована намного сложнее, и его было труднее воспринимать. + +В предыдущих версиях Python для этого использовались потоки или Gevent. Но такой код намного сложнее понимать, отлаживать и мысленно представлять. + +Что касается JavaScript (в браузере и NodeJS), раньше там использовали для этой цели +"обратные вызовы". Что выливалось в +ад обратных вызовов. + +## Сопрограммы + +**Корути́на** (или же сопрограмма) — это крутое словечко для именования той сущности, +которую возвращает функция `async def`. Python знает, что её можно запустить, как и обычную функцию, +но кроме того сопрограмму можно поставить на паузу ⏸ в том месте, где встретится слово `await`. + +Всю функциональность асинхронного программирования с использованием `async` и `await` +часто обобщают словом "корутины". Они аналогичны "горутинам", ключевой особенности +языка Go. + +## Заключение + +В самом начале была такая фраза: + +> Современные версии Python поддерживают разработку так называемого +**"асинхронного кода"** посредством написания **"сопрограмм"** с использованием +синтаксиса **`async` и `await`**. + +Теперь всё должно звучать понятнее. ✨ + +На этом основана работа FastAPI (посредством Starlette), и именно это +обеспечивает его высокую производительность. + +## Очень технические подробности + +!!! warning + Этот раздел читать не обязательно. + + Здесь приводятся подробности внутреннего устройства **FastAPI**. + + Но если вы обладаете техническими знаниями (корутины, потоки, блокировка и т. д.) + и вам интересно, как FastAPI обрабатывает `async def` в отличие от обычных `def`, + читайте дальше. + +### Функции обработки пути + +Когда вы объявляете *функцию обработки пути* обычным образом с ключевым словом `def` +вместо `async def`, FastAPI ожидает её выполнения, запустив функцию во внешнем +пуле потоков, а не напрямую (это бы заблокировало сервер). + +Если ранее вы использовали другой асинхронный фреймворк, который работает иначе, +и привыкли объявлять простые вычислительные *функции* через `def` ради +незначительного прироста скорости (порядка 100 наносекунд), обратите внимание, +что с **FastAPI** вы получите противоположный эффект. В таком случае больше подходит +`async def`, если только *функция обработки пути* не использует код, приводящий +к блокировке I/O. + + + +Но в любом случае велика вероятность, что **FastAPI** [окажется быстрее](/#performance){.internal-link target=_blank} +другого фреймворка (или хотя бы на уровне с ним). + +### Зависимости + +То же относится к зависимостям. Если это обычная функция `def`, а не `async def`, +она запускается во внешнем пуле потоков. + +### Подзависимости + +Вы можете объявить множество ссылающихся друг на друга зависимостей и подзависимостей +(в виде параметров при определении функции). Какие-то будут созданы с помощью `async def`, +другие обычным образом через `def`, и такая схема вполне работоспособна. Функции, +объявленные с помощью `def` будут запускаться на внешнем потоке (из пула), +а не с помощью `await`. + +### Другие служебные функции + +Любые другие служебные функции, которые вы вызываете напрямую, можно объявлять +с использованием `def` или `async def`. FastAPI не будет влиять на то, как вы +их запускаете. + +Этим они отличаются от функций, которые FastAPI вызывает самостоятельно: +*функции обработки пути* и зависимости. + +Если служебная функция объявлена с помощью `def`, она будет вызвана напрямую +(как вы и написали в коде), а не в отдельном потоке. Если же она объявлена с +помощью `async def`, её вызов должен осуществляться с ожиданием через `await`. + +--- + + +Ещё раз повторим, что все эти технические подробности полезны, только если вы специально их искали. + +В противном случае просто ознакомьтесь с основными принципами в разделе выше: Нет времени?. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 213f941d7..0f8f00411 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -54,6 +54,7 @@ nav: - tr: /tr/ - uk: /uk/ - zh: /zh/ +- async.md markdown_extensions: - toc: permalink: true