# Конкурентность и async / await { #concurrency-and-async-await }

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

## Нет времени? { #in-a-hurry }

<abbr title="too long; didn't read - слишком длинно; не читал"><strong>TL;DR:</strong></abbr>

Если вы используете сторонние библиотеки, которые нужно вызывать с `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`, даже если внутри не нужен `await`.

---

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

---

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

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

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

## Технические подробности { #technical-details }

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

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

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

## Асинхронный код { #asynchronous-code }

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

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

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

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

Это «ожидание чего-то ещё» обычно относится к операциям <abbr title="Input and Output - Ввод/вывод">I/O</abbr>, которые относительно «медленные» (по сравнению со скоростью процессора и оперативной памяти), например ожидание:

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

Поскольку основное время выполнения уходит на ожидание операций <abbr title="Input and Output - Ввод/вывод">I/O</abbr>, их называют операциями, «ограниченными вводом-выводом» (I/O bound).

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

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

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

### Конкурентность и бургеры { #concurrency-and-burgers }

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

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

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

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

### Конкурентные бургеры { #concurrent-burgers }

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

<img src="/img/async/concurrent-burgers/concurrent-burgers-01.png" class="illustration">

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

<img src="/img/async/concurrent-burgers/concurrent-burgers-02.png" class="illustration">

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

<img src="/img/async/concurrent-burgers/concurrent-burgers-03.png" class="illustration">

Вы платите. 💸

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

<img src="/img/async/concurrent-burgers/concurrent-burgers-04.png" class="illustration">

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

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

<img src="/img/async/concurrent-burgers/concurrent-burgers-05.png" class="illustration">

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

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

<img src="/img/async/concurrent-burgers/concurrent-burgers-06.png" class="illustration">

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

<img src="/img/async/concurrent-burgers/concurrent-burgers-07.png" class="illustration">

/// info | Информация

Прекрасные иллюстрации от [Ketrina Thompson](https://www.instagram.com/ketrinadrawsalot). 🎨

///

---

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

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

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

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

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

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

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

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

### Параллельные бургеры { #parallel-burgers }

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

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

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

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

<img src="/img/async/parallel-burgers/parallel-burgers-01.png" class="illustration">

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

Вы платите 💸.

<img src="/img/async/parallel-burgers/parallel-burgers-02.png" class="illustration">

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

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

<img src="/img/async/parallel-burgers/parallel-burgers-03.png" class="illustration">

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

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

<img src="/img/async/parallel-burgers/parallel-burgers-04.png" class="illustration">

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

<img src="/img/async/parallel-burgers/parallel-burgers-05.png" class="illustration">

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

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

<img src="/img/async/parallel-burgers/parallel-burgers-06.png" class="illustration">

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

/// info | Информация

Прекрасные иллюстрации от [Ketrina Thompson](https://www.instagram.com/ketrinadrawsalot). 🎨

///

---

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

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

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

---

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

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

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

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

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

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

### Вывод про бургеры { #burger-conclusion }

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

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

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

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

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

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

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

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

А так как можно одновременно использовать параллелизм и асинхронность, вы получаете производительность выше, чем у большинства протестированных фреймворков на NodeJS и на уровне Go, который — компилируемый язык, ближе к C [(всё благодаря Starlette)](https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=query&l=zijmkf-1).

### Конкурентность лучше параллелизма? { #is-concurrency-better-than-parallelism }

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

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

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

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

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

---

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

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

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

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

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

И так как основное время выполнения уходит на реальную работу (а не ожидание), а работу в компьютере выполняет <abbr title="Central Processing Unit - Центральный процессор">CPU</abbr>, такие задачи называют «ограниченными процессором» (CPU bound).

---

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

Например:

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

### Конкурентность + параллелизм: Веб + Машинное обучение { #concurrency-parallelism-web-machine-learning }

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

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

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

Как добиться такого параллелизма в продакшн, см. раздел [Развёртывание](deployment/index.md).

## `async` и `await` { #async-and-await }

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

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

```Python
burgers = await get_burgers(2)
```

Ключ здесь — `await`. Он говорит Python, что нужно подождать ⏸, пока `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` Python знает, что внутри этой функции нужно учитывать выражения `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
```

### Более технические подробности { #more-technical-details }

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

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

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

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

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

### Пишите свой асинхронный код { #write-your-own-async-code }

Starlette (и **FastAPI**) основаны на [AnyIO](https://anyio.readthedocs.io/en/stable/), что делает их совместимыми и со стандартной библиотекой Python [asyncio](https://docs.python.org/3/library/asyncio-task.html), и с [Trio](https://trio.readthedocs.io/en/stable/).

В частности, вы можете напрямую использовать [AnyIO](https://anyio.readthedocs.io/en/stable/) для продвинутых сценариев конкурентности, где в вашем коде нужны более сложные паттерны.

И даже если вы не используете FastAPI, вы можете писать свои асинхронные приложения с [AnyIO](https://anyio.readthedocs.io/en/stable/), чтобы они были максимально совместимыми и получали его преимущества (например, *структурную конкурентность*).

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

### Другие формы асинхронного кода { #other-forms-of-asynchronous-code }

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

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

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

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

В предыдущих версиях Python можно было использовать потоки или [Gevent](https://www.gevent.org/). Но такой код гораздо сложнее понимать, отлаживать и держать в голове.

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

## Сопрограммы { #coroutines }

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

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

## Заключение { #conclusion }

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

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

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

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

## Очень технические подробности { #very-technical-details }

/// warning | Предупреждение

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

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

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

///

### Функции-обработчики пути { #path-operation-functions }

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

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

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

### Зависимости { #dependencies }

То же относится к [зависимостям](tutorial/dependencies/index.md). Если зависимость — это обычная функция `def`, а не `async def`, она запускается во внешнем пуле потоков.

### Подзависимости { #sub-dependencies }

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

### Другие служебные функции { #other-utility-functions }

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

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

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

---

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

Иначе вам достаточно руководствоваться рекомендациями из раздела выше: <a href="#in-a-hurry">Нет времени?</a>.
