fastapi/docs/ru/docs/async.md

512 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Конкурентность и async / await
Здесь приведена подробная информация об использовании синтаксиса `async def` при написании *функций обработки пути*, а также рассмотрены основы асинхронного программирования, конкурентности и параллелизма.
## Нет времени?<a name="in-a-hurry"></a>
<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`.
---
Если вы не уверены, используйте обычный синтаксис `def`.
---
**Примечание**: при необходимости можно смешивать `def` и `async def` в *функциях обработки пути*
и использовать в каждом случае наиболее подходящий синтаксис. А FastAPI сделает с этим всё, что нужно.
В любом из описанных случаев FastAPI работает асинхронно и очень быстро.
Однако придерживаясь указанных советов, можно получить дополнительную оптимизацию производительности.
## Технические подробности
Современные версии Python поддерживают разработку так называемого **"асинхронного кода"** посредством написания **"сопрограмм"** с использованием синтаксиса **`async` и `await`**.
Ниже разберём эту фразу по частям:
* **Асинхронный код**
* **`async` и `await`**
* **Сопрограммы**
## Асинхронный код
Асинхронный код означает, что в языке 💬 есть возможность сообщить машине / программе 🤖,
что в определённой точке кода ей 🤖 нужно будет ожидать завершения выполнения *чего-то ещё* в другом месте. Допустим это *что-то ещё* называется "медленный файл" 📝.
И пока мы ждём завершения работы с "медленным файлом" 📝, компьютер может переключиться для выполнения других задач.
Но при каждой возможности компьютер / программа 🤖 будет возвращаться обратно. Например, если он 🤖 опять окажется в режиме ожидания, или когда закончит всю работу. В этом случае компьютер 🤖 проверяет, не завершена ли какая-нибудь из текущих задач.
Потом он 🤖 берёт первую выполненную задачу (допустим, наш "медленный файл" 📝) и продолжает работу, производя с ней необходимые действия.
Вышеупомянутое "что-то ещё", завершения которого приходится ожидать, обычно относится к достаточно "медленным" операциям <abbr title="Ввода-вывода">I/O</abbr> (по сравнению со скоростью работы процессора и оперативной памяти), например:
* отправка данных от клиента по сети
* получение клиентом данных, отправленных вашей программой по сети
* чтение системой содержимого файла с диска и передача этих данных программе
* запись на диск данных, которые программа передала системе
* обращение к удалённому API
* ожидание завершения операции с базой данных
* получение результатов запроса к базе данных
* и т. д.
Поскольку в основном время тратится на ожидание выполнения операций <abbr title="Ввода-вывода">I/O</abbr>,
их обычно называют операциями, <abbr title="I/O bound">ограниченными скоростью ввода-вывода</abbr>.
Код называют "асинхронным", потому что компьютеру / программе не требуется "синхронизироваться" с медленной задачей и,
будучи в простое, ожидать момента её завершения, с тем чтобы забрать результат и продолжить работу.
Вместо этого в "асинхронной" системе завершённая задача может немного подождать (буквально несколько микросекунд),
пока компьютер / программа занимается другими важными вещами, с тем чтобы потом вернуться,
забрать результаты выполнения и начать их обрабатывать.
"Синхронное" исполнение (в противовес "асинхронному") также называют <abbr title="sequential">"последовательным"</abbr>,
потому что компьютер / программа последовательно выполняет все требуемые шаги перед тем, как перейти к следующей задаче,
даже если в процессе приходится ждать.
### Конкурентность и бургеры
Тот **асинхронный** код, о котором идёт речь выше, иногда называют **"конкурентностью"**. Она отличается от **"параллелизма"**.
Да, **конкурентность** и **параллелизм** подразумевают, что разные вещи происходят примерно в одно время.
Но внутреннее устройство **конкурентности** и **параллелизма** довольно разное.
Чтобы это понять, представьте такую картину:
### Конкурентные бургеры
<!-- The gender neutral cook emoji "🧑‍🍳" does not render well in browsers. In the meantime, I'm using a mix of male "👨‍🍳" and female "👩‍🍳" cooks. -->
Вы идёте со своей возлюбленной 😍 в фастфуд 🍔 и становитесь в очередь, в это время кассир 💁 принимает заказы у посетителей перед вами.
Когда наконец подходит очередь, вы заказываете парочку самых вкусных и навороченных бургеров 🍔, один для своей возлюбленной 😍, а другой себе.
Отдаёте деньги 💸.
Кассир 💁 что-то говорит поварам на кухне 👨‍🍳, теперь они знают, какие бургеры нужно будет приготовить 🍔
(но пока они заняты бургерами предыдущих клиентов).
Кассир 💁 отдаёт вам чек с номером заказа.
В ожидании еды вы идёте со своей возлюбленной 😍 выбрать столик, садитесь и довольно продолжительное время общаетесь 😍
(поскольку ваши бургеры самые навороченные, готовятся они не так быстро ✨🍔✨).
Сидя за столиком с возлюбленной 😍 в ожидании бургеров 🍔, вы отлично проводите время,
восхищаясь её великолепием, красотой и умом ✨😍✨.
Всё ещё ожидая заказ и болтая со своей возлюбленной 😍, время от времени вы проверяете,
какой номер горит над прилавком, и не подошла ли уже ваша очередь.
И вот наконец настаёт этот момент, и вы идёте к стойке, чтобы забрать бургеры 🍔 и вернуться за столик.
Вы со своей возлюбленной 😍 едите бургеры 🍔 и отлично проводите время ✨.
---
А теперь представьте, что в этой небольшой истории вы компьютер / программа 🤖.
В очереди вы просто глазеете по сторонам 😴, ждёте и ничего особо "продуктивного" не делаете.
Но очередь движется довольно быстро, поскольку кассир 💁 только принимает заказы (а не занимается приготовлением еды), так что ничего страшного.
Когда подходит очередь вы наконец предпринимаете "продуктивные" действия 🤓: просматриваете меню, выбираете в нём что-то, узнаёте, что хочет ваша возлюбленная 😍, собираетесь оплатить 💸, смотрите, какую достали карту, проверяете, чтобы с вас списали верную сумму, и что в заказе всё верно и т. д.
И хотя вы всё ещё не получили бургеры 🍔, ваша работа с кассиром 💁 ставится "на паузу" ⏸,
поскольку теперь нужно ждать 🕙, когда заказ приготовят.
Но отойдя с номерком от прилавка, вы садитесь за столик и можете переключить 🔀 внимание
на свою возлюбленную 😍 и "работать" ⏯ 🤓 уже над этим. И вот вы снова очень
"продуктивны" 🤓, мило болтаете вдвоём и всё такое 😍.
В какой-то момент кассир 💁 поместит на табло ваш номер, подразумевая, что бургеры готовы 🍔, но вы не станете подскакивать как умалишённый, лишь только увидев на экране свою очередь. Вы уверены, что ваши бургеры 🍔 никто не утащит, ведь у вас свой номерок, а у других свой.
Поэтому вы подождёте, пока возлюбленная 😍 закончит рассказывать историю (закончите текущую работу ⏯ / задачу в обработке 🤓),
и мило улыбнувшись, скажете, что идёте забирать заказ ⏸.
И вот вы подходите к стойке 🔀, к первоначальной задаче, которая уже завершена ⏯, берёте бургеры 🍔, говорите спасибо и относите заказ за столик. На этом заканчивается этап / задача взаимодействия с кассой ⏹.
В свою очередь порождается задача "поедание бургеров" 🔀 ⏯, но предыдущая ("получение бургеров") завершена ⏹.
### Параллельные бургеры
Теперь представим, что вместо бургерной "Конкурентные бургеры" вы решили сходить в "Параллельные бургеры".
И вот вы идёте со своей возлюбленной 😍 отведать параллельного фастфуда 🍔.
Вы становитесь в очередь пока несколько (пусть будет 8) кассиров, которые по совместительству ещё и повары 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳, принимают заказы у посетителей перед вами.
При этом клиенты не отходят от стойки и ждут 🕙 получения еды, поскольку каждый
из 8 кассиров идёт на кухню готовить бургеры 🍔, а только потом принимает следующий заказ.
Наконец настаёт ваша очередь, и вы просите два самых навороченных бургера 🍔, один для дамы сердца 😍, а другой себе.
Ни о чём не жалея, расплачиваетесь 💸.
И кассир уходит на кухню 👨‍🍳.
Вам приходится ждать перед стойкой 🕙, чтобы никто по случайности не забрал ваши бургеры 🍔, ведь никаких номерков у вас нет.
Поскольку вы с возлюбленной 😍 хотите получить заказ вовремя 🕙, и следите за тем, чтобы никто не вклинился в очередь,
у вас не получается уделять должного внимание своей даме сердца 😞.
Это "синхронная" работа, вы "синхронизированы" с кассиром/поваром 👨‍🍳. Приходится ждать 🕙 у стойки,
когда кассир/повар 👨‍🍳 закончит делать бургеры 🍔 и вручит вам заказ, иначе его случайно может забрать кто-то другой.
Наконец кассир/повар 👨‍🍳 возвращается с бургерами 🍔 после невыносимо долгого ожидания 🕙 за стойкой.
Вы скорее забираете заказ 🍔 и идёте с возлюбленной 😍 за столик.
Там вы просто едите эти бургеры, и на этом всё 🍔 ⏹.
Вам не особо удалось пообщаться, потому что большую часть времени 🕙 пришлось провести у кассы 😞.
---
В описанном сценарии вы компьютер / программа 🤖 с двумя исполнителями (вы и ваша возлюбленная 😍),
на протяжении долгого времени 🕙 вы оба уделяете всё внимание ⏯ задаче "ждать на кассе".
В этом ресторане быстрого питания 8 исполнителей (кассиров/поваров) 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳.
Хотя в бургерной конкурентного типа было всего два (один кассир и один повар) 💁 👨‍🍳.
Несмотря на обилие работников, опыт в итоге получился не из лучших 😞.
---
Так бы выглядел аналог истории про бургерную 🍔 в "параллельном" мире.
Вот более реалистичный пример. Представьте себе банк.
До недавних пор в большинстве банков было несколько кассиров 👨‍💼👨‍💼👨‍💼👨‍💼 и длинные очереди 🕙🕙🕙🕙🕙🕙🕙🕙.
Каждый кассир обслуживал одного клиента, потом следующего 👨‍💼⏯.
Нужно было долгое время 🕙 стоять перед окошком вместе со всеми, иначе пропустишь свою очередь.
Сомневаюсь, что у вас бы возникло желание прийти с возлюбленной 😍 в банк 🏦 оплачивать налоги.
### Выводы о бургерах
В нашей истории про поход в фастфуд за бургерами приходится много ждать 🕙,
поэтому имеет смысл организовать конкурентную систему ⏸🔀⏯.
И то же самое с большинством веб-приложений.
Пользователей очень много, но ваш сервер всё равно вынужден ждать 🕙 запросы по их слабому интернет-соединению.
Потом снова ждать 🕙, пока вернётся ответ.
<!--https://forum.wordreference.com/threads/%D0%9D%D0%BE-%D0%B5%D1%81%D0%BB%D0%B8.3258695/-->
Это ожидание 🕙 измеряется микросекундами, но если всё сложить, то набегает довольно много времени.
Вот почему есть смысл использовать асинхронное ⏸🔀⏯ программирование при построении веб-API.
Большинство популярных фреймворков (включая Flask и Django) создавались
до появления в Python новых возможностей асинхронного программирования. Поэтому
их можно разворачивать с поддержкой параллельного исполнения или асинхронного
программирования старого типа, которое не настолько эффективно.
При том, что основная спецификация асинхронного взаимодействия Python с веб-сервером
(<a href="https://asgi.readthedocs.io" class="external-link" target="_blank">ASGI</a>)
была разработана командой Django для внедрения поддержки веб-сокетов.
Именно асинхронность сделала NodeJS таким популярным (несмотря на то, что он не параллельный),
и в этом преимущество Go как языка программирования.
И тот же уровень производительности даёт **FastAPI**.
Поскольку можно использовать преимущества параллелизма и асинхронности вместе,
вы получаете производительность лучше, чем у большинства протестированных NodeJS фреймворков
и на уровне с Go, который является компилируемым языком близким к C <a href="https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=query&l=zijmkf-1" class="external-link" target="_blank">(всё благодаря Starlette)</a>.
### Получается, конкурентность лучше параллелизма?
Нет! Мораль истории совсем не в этом.
Конкурентность отличается от параллелизма. Она лучше в **конкретных** случаях, где много времени приходится на ожидание.
Вот почему она зачастую лучше параллелизма при разработке веб-приложений. Но это не значит, что конкурентность лучше в любых сценариях.
Давайте посмотрим с другой стороны, представьте такую картину:
> Вам нужно убраться в большом грязном доме.
*Да, это вся история*.
---
Тут не нужно нигде ждать 🕙, просто есть куча работы в разных частях дома.
Можно организовать очередь как в примере с бургерами, сначала гостиная, потом кухня,
но это ни на что не повлияет, поскольку вы нигде не ждёте 🕙, а просто трёте да моете.
И понадобится одинаковое количество времени с очередью (конкурентностью) и без неё,
и работы будет сделано тоже одинаковое количество.
Однако в случае, если бы вы могли привести 8 бывших кассиров/поваров, а ныне уборщиков 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳,
и каждый из них (вместе с вами) взялся бы за свой участок дома,
с такой помощью вы бы закончили намного быстрее, делая всю работу **параллельно**.
В описанном сценарии каждый уборщик (включая вас) был бы исполнителем, занятым на своём участке работы.
И поскольку большую часть времени выполнения занимает реальная работа (а не ожидание),
а работу в компьютере делает <abbr title="Центральный процессор (CPU)">ЦП</abbr>,
такие задачи называют <abbr title="CPU bound">ограниченными производительностью процессора</abbr>.
---
Ограничение по процессору проявляется в операциях, где требуется выполнять сложные математические вычисления.
Например:
* Обработка **звука** или **изображений**.
* **Компьютерное зрение**: изображение состоит из миллионов пикселей, в каждом пикселе 3 составляющих цвета,
обработка обычно требует проведения расчётов по всем пикселям сразу.
* **Машинное обучение**: здесь обычно требуется умножение "матриц" и "векторов".
Представьте гигантскую таблицу с числами в Экселе, и все их надо одновременно перемножить.
* **Глубокое обучение**: это область *машинного обучения*, поэтому сюда подходит то же описание.
Просто у вас будет не одна таблица в Экселе, а множество. В ряде случаев используется
специальный процессор для создания и / или использования построенных таким образом моделей.
### Конкурентность + параллелизм: Веб + машинное обучение
**FastAPI** предоставляет возможности конкуретного программирования,
которое очень распространено в веб-разработке (именно этим славится NodeJS).
Кроме того вы сможете использовать все преимущества параллелизма и
<abbr title="multiprocessing">многопроцессорности</abbr> (когда несколько процессов работают параллельно),
если рабочая нагрузка предполагает **ограничение по процессору**,
как, например, в системах машинного обучения. <!--http://new.gramota.ru/spravka/punctum?layout=item&id=58_329-->
Необходимо также отметить, что Python является главным языком в области
<abbr title="наука о данных (data science)">**дата-сайенс**</abbr>,
машинного обучения и, особенно, глубокого обучения. Всё это делает 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 может пока переключиться на выполнение других задач 🔀 ⏯
(например получение следующего запроса).<!--http://new.gramota.ru/spravka/buro/search-answer?s=296614-->
Чтобы ключевое слово `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`, вам нужно <abbr title="await">"ожидать"</abbr> её.
Поэтому такое не сработает:
```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`.
<!--http://new.gramota.ru/spravka/punctum?layout=item&id=58_128-->
Но выполнение такой функции необходимо "ожидать" с помощью `await`.
Это означает, что её можно вызвать только из другой функции, которая тоже объявлена с `async def`.
Но как же тогда появилась первая <abbr title="или яйцо?🤔">курица</abbr>? В смысле... как нам вызвать первую асинхронную функцию?
При работе с **FastAPI** просто не думайте об этом, потому что "первой" функцией является ваша *функция обработки пути*,
и дальше с этим разберётся FastAPI.
Кроме того, если хотите, вы можете использовать синтаксис `async` / `await` и без FastAPI.
### Пишите свой асинхронный код
Starlette (и **FastAPI**) основаны на <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>, что делает их совместимыми как со стандартной библиотекой <a href="https://docs.python.org/3/library/asyncio-task.html" class="external-link" target="_blank">asyncio</a> в Python, так и с <a href="https://trio.readthedocs.io/en/stable/" class="external-link" target="_blank">Trio</a>.<!--http://new.gramota.ru/spravka/buro/search-answer?s=285295-->
В частности, вы можете напрямую использовать <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a> в тех проектах, где требуется более сложная логика работы с конкурентностью.
Даже если вы не используете FastAPI, вы можете писать асинхронные приложения с помощью <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>, чтобы они были максимально совместимыми и получали его преимущества (например *структурную конкурентность*).
### Другие виды асинхронного программирования
Стиль написания кода с `async` и `await` появился в языке Python относительно недавно.
Но он сильно облегчает работу с асинхронным кодом.
Ровно такой же синтаксис (ну или почти такой же) недавно был включён в современные версии JavaScript (в браузере и NodeJS).
До этого поддержка асинхронного кода была реализована намного сложнее, и его было труднее воспринимать.
В предыдущих версиях Python для этого использовались потоки или <a href="https://www.gevent.org/" class="external-link" target="_blank">Gevent</a>. Но такой код намного сложнее понимать, отлаживать и мысленно представлять.
Что касается JavaScript (в браузере и NodeJS), раньше там использовали для этой цели
<abbr title="callback">"обратные вызовы"</abbr>. Что выливалось в
"ад обратных вызовов".
## Сопрограммы
<abbr title="coroutine">**Корути́на**</abbr> (или же сопрограмма) — это крутое словечко для именования той сущности,
которую возвращает функция `async def`. Python знает, что её можно запустить, как и обычную функцию,
но кроме того сопрограмму можно поставить на паузу ⏸ в том месте, где встретится слово `await`.
Всю функциональность асинхронного программирования с использованием `async` и `await`
часто обобщают словом "корутины". Они аналогичны <abbr title="Goroutines">"горутинам"</abbr>, ключевой особенности
языка Go.
## Заключение
В самом начале была такая фраза:
> Современные версии Python поддерживают разработку так называемого
**"асинхронного кода"** посредством написания **"сопрограмм"** с использованием
синтаксиса **`async` и `await`**.
Теперь всё должно звучать понятнее. ✨
На этом основана работа FastAPI (посредством Starlette), и именно это
обеспечивает его высокую производительность.
## Очень технические подробности
/// warning
Этот раздел читать не обязательно.
Здесь приводятся подробности внутреннего устройства **FastAPI**.
Но если вы обладаете техническими знаниями (корутины, потоки, блокировка и т. д.)
и вам интересно, как FastAPI обрабатывает `async def` в отличие от обычных `def`,
читайте дальше.
///
### Функции обработки пути
Когда вы объявляете *функцию обработки пути* обычным образом с ключевым словом `def`
вместо `async def`, FastAPI ожидает её выполнения, запустив функцию во внешнем
<abbr title="threadpool">пуле потоков</abbr>, а не напрямую (это бы заблокировало сервер).
Если ранее вы использовали другой асинхронный фреймворк, который работает иначе,
и привыкли объявлять простые вычислительные *функции* через `def` ради
незначительного прироста скорости (порядка 100 наносекунд), обратите внимание,
что с **FastAPI** вы получите противоположный эффект. В таком случае больше подходит
`async def`, если только *функция обработки пути* не использует код, приводящий
к блокировке <abbr title="Ввод/вывод: чтение и запись на диск, сетевые соединения.">I/O</abbr>.
<!--Уточнить: Не использовать async def, если код приводит к блокировке IO?-->
<!--http://new.gramota.ru/spravka/punctum?layout=item&id=58_285-->
Но в любом случае велика вероятность, что **FastAPI** [окажется быстрее](index.md#_11){.internal-link target=_blank}
другого фреймворка (или хотя бы на уровне с ним).
### Зависимости
То же относится к зависимостям. Если это обычная функция `def`, а не `async def`,
она запускается во внешнем пуле потоков.
### Подзависимости
Вы можете объявить множество ссылающихся друг на друга зависимостей и подзависимостей
(в виде параметров при определении функции). Какие-то будут созданы с помощью `async def`,
другие обычным образом через `def`, и такая схема вполне работоспособна. Функции,
объявленные с помощью `def` будут запускаться на внешнем потоке (из пула),
а не с помощью `await`.
### Другие служебные функции
Любые другие служебные функции, которые вы вызываете напрямую, можно объявлять
с использованием `def` или `async def`. FastAPI не будет влиять на то, как вы
их запускаете.
Этим они отличаются от функций, которые FastAPI вызывает самостоятельно:
*функции обработки пути* и зависимости.
Если служебная функция объявлена с помощью `def`, она будет вызвана напрямую
(как вы и написали в коде), а не в отдельном потоке. Если же она объявлена с
помощью `async def`, её вызов должен осуществляться с ожиданием через `await`.
---
<!--http://new.gramota.ru/spravka/buro/search-answer?s=299749-->
Ещё раз повторим, что все эти технические подробности полезны, только если вы специально их искали.
В противном случае просто ознакомьтесь с основными принципами в разделе выше: <a href="#_1">Нет времени?</a>.