# Рівночасність і async / await { #concurrency-and-async-await } Деталі щодо синтаксису `async def` для функцій операції шляху і деякі відомості про асинхронний код, рівночасність і паралелізм. ## Поспішаєте? { #in-a-hurry } 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`, навіть якщо вам не потрібно використовувати `await` всередині. --- Якщо ви не певні, використовуйте звичайний `def`. --- Примітка: ви можете змішувати `def` і `async def` у ваших функціях операції шляху скільки завгодно і визначати кожну з них найкращим для вас способом. FastAPI зробить з ними все правильно. У будь-якому з наведених випадків FastAPI все одно працюватиме асинхронно і буде надзвичайно швидким. Але слідуючи крокам вище, він зможе зробити деякі оптимізації продуктивності. ## Технічні деталі { #technical-details } Сучасні версії Python мають підтримку «асинхронного коду» за допомогою так званих «співпрограм» з синтаксисом **`async` і `await`**. Розгляньмо цю фразу по частинах у секціях нижче: - Асинхронний код - `async` і `await` - Співпрограми ## Асинхронний код { #asynchronous-code } Асинхронний код означає, що мова 💬 має спосіб сказати комп’ютеру/програмі 🤖, що в певний момент у коді він 🤖 має почекати, поки «щось інше» завершиться десь ще. Скажімо, це «щось інше» називається «slow-file» 📝. Отже, в цей час комп’ютер може піти і зробити іншу роботу, доки «slow-file» 📝 завершується. Далі комп’ютер/програма 🤖 повертатиметься щоразу, коли матиме можливість, бо знову чекає, або коли він 🤖 завершив усю роботу, яка була в нього на той момент. І він 🤖 перевірить, чи якась із задач, на які він чекав, уже завершилася, виконавши все, що потрібно. Потім він 🤖 бере першу завершену задачу (скажімо, наш «slow-file» 📝) і продовжує робити те, що потрібно було зробити з нею. Це «чекати на щось інше» зазвичай стосується операцій I/O, які відносно «повільні» (порівняно зі швидкістю процесора та пам’яті з довільним доступом), наприклад, очікування: - даних від клієнта, що надсилаються мережею - даних, надісланих вашим застосунком, які клієнт має отримати мережею - вмісту файла на диску, який система має прочитати і передати вашому застосунку - вмісту, який ваш застосунок передав системі, щоб він був записаний на диск - віддаленої операції API - завершення операції бази даних - повернення результатів запиту до бази даних - тощо Оскільки час виконання переважно витрачається на очікування операцій I/O, їх називають операціями «I/O bound». Це називається «асинхронним», тому що комп’ютеру/програмі не потрібно бути «синхронізованими» з повільною задачею, очікуючи точного моменту її завершення, нічого не роблячи, лише щоб отримати результат задачі та продовжити роботу. Натомість, у «асинхронній» системі щойно завершена задача може трохи зачекати в черзі (кілька мікросекунд), доки комп’ютер/програма завершить те, що пішов робити, а потім повернеться, щоб забрати результати і продовжити роботу з ними. Для «синхронного» (на противагу «асинхронному») часто також використовують термін «послідовний», бо комп’ютер/програма слідує всім крокам послідовно, перш ніж перемкнутися на іншу задачу, навіть якщо ці кроки включають очікування. ### Рівночасність і бургери { #concurrency-and-burgers } Ідею **асинхронного** коду, описану вище, інколи також називають **«рівночасністю»**. Вона відрізняється від **«паралелізму»**. І рівночасність, і паралелізм стосуються «різних речей, що відбуваються більш-менш одночасно». Але деталі між рівночасністю і паралелізмом досить різні. Щоб побачити різницю, уявімо таку історію про бургери: ### Рівночасні бургери { #concurrent-burgers } Ви йдете зі своєю симпатією по фастфуд, стаєте в чергу, доки касир приймає замовлення у людей перед вами. 😍 Потім ваша черга, ви замовляєте 2 дуже вишукані бургери для вашої симпатії і для себе. 🍔🍔 Касир каже щось кухарю на кухні, щоб той знав, що треба приготувати ваші бургери (хоча зараз він готує бургери для попередніх клієнтів). Ви платите. 💸 Касир дає вам номер вашої черги. Поки ви чекаєте, ви з вашою симпатією обираєте столик, сідаєте і довго розмовляєте (адже ваші бургери дуже вишукані і потребують часу на приготування). Сидячи за столиком із вашою симпатією, доки чекаєте бургери, ви можете витратити цей час, милуючись тим, яка ваша симпатія класна, мила і розумна ✨😍✨. Чекаючи і спілкуючись із вашою симпатією, час від часу ви перевіряєте номер на табло біля прилавка, щоб побачити, чи вже ваша черга. І от нарешті ваша черга. Ви підходите до прилавка, забираєте бургери і повертаєтеся до столика. Ви з вашою симпатією їсте бургери і гарно проводите час. ✨ /// info | Інформація Прекрасні ілюстрації від Ketrina Thompson. 🎨 /// --- Уявіть, що в цій історії ви - комп’ютер/програма 🤖. Поки ви в черзі, ви просто бездіяльні 😴, чекаєте своєї черги, нічого «продуктивного» не роблячи. Але черга рухається швидко, бо касир лише приймає замовлення (а не готує їх), тож це нормально. Коли ж ваша черга, ви виконуєте справді «продуктивну» роботу: переглядаєте меню, вирішуєте, що бажаєте, дізнаєтеся вибір вашої симпатії, платите, перевіряєте, що віддаєте правильну купюру чи картку, що з вас правильно списали кошти, що замовлення містить правильні позиції тощо. Але потім, хоча у вас ще немає бургерів, ваша взаємодія з касиром «на паузі» ⏸, бо вам доводиться чекати 🕙, поки бургери будуть готові. Втім, відійшовши від прилавка і сівши за столик із номерком, ви можете перемкнути 🔀 увагу на свою симпатію і «попрацювати» ⏯ 🤓 над цим. Тоді ви знову робите щось дуже «продуктивне» - фліртуєте зі своєю симпатією 😍. Потім касир 💁 каже «Я закінчив робити бургери», виводячи ваш номер на табло прилавка, але ви не підстрибуєте миттєво, щойно номер змінюється на ваш. Ви знаєте, що ніхто не вкраде ваші бургери, адже у вас є номер вашої черги, а в інших - свій. Тож ви чекаєте, поки ваша симпатія завершить історію (завершить поточну роботу ⏯/задачу 🤓), лагідно усміхаєтеся і кажете, що підете за бургерами ⏸. Потім ви йдете до прилавка 🔀, до початкової задачі, яку тепер завершено ⏯, забираєте бургери, дякуєте і несете їх до столу. Це завершує той крок/задачу взаємодії з прилавком ⏹. Натомість з’являється нова задача «їсти бургери» 🔀 ⏯, але попередня «отримати бургери» завершена ⏹. ### Паралельні бургери { #parallel-burgers } А тепер уявімо, що це не «рівночасні бургери», а «паралельні бургери». Ви йдете зі своєю симпатією по паралельний фастфуд. Ви стаєте в чергу, поки кілька (скажімо, 8) касирів, які водночас є кухарями, приймають замовлення у людей перед вами. Кожен перед вами чекає, поки його бургери будуть готові, перш ніж відійти від прилавка, тому що кожен з 8 касирів одразу йде і готує бургер, перш ніж приймати наступне замовлення. Нарешті ваша черга, ви замовляєте 2 дуже вишукані бургери для вашої симпатії і для себе. Ви платите 💸. Касир іде на кухню. Ви чекаєте, стоячи перед прилавком 🕙, щоб ніхто інший не забрав ваші бургери раніше, ніж ви, адже номерків черги немає. Оскільки ви з вашою симпатією зайняті тим, щоб ніхто не став перед вами і не забрав ваші бургери, щойно вони з’являться, ви не можете приділяти увагу своїй симпатії. 😞 Це «синхронна» робота, ви «синхронізовані» з касиром/кухарем 👨‍🍳. Вам доводиться чекати 🕙 і бути тут у точний момент, коли касир/кухар 👨‍🍳 завершить бургери і віддасть їх вам, інакше хтось інший може їх забрати. Потім ваш касир/кухар 👨‍🍳 нарешті повертається з вашими бургерами після довгого очікування 🕙 перед прилавком. Ви берете бургери і йдете до столика зі своєю симпатією. Ви просто їх їсте - і все. ⏹ Багато розмов чи флірту не було, бо більшість часу пішла на очікування 🕙 перед прилавком. 😞 /// info | Інформація Прекрасні ілюстрації від Ketrina Thompson. 🎨 /// --- У цьому сценарії паралельних бургерів ви - комп’ютер/програма 🤖 з двома процесорами (ви і ваша симпатія), які обидва чекають 🕙 і приділяють увагу ⏯ «очікуванню біля прилавка» 🕙 тривалий час. У закладу фастфуду 8 процесорів (касира/кухаря). У той час як у закладі з рівночасними бургерами могло бути лише 2 (один касир і один кухар). Та все одно фінальний досвід не найкращий. 😞 --- Це була б паралельна історія про бургери. 🍔 Для більш «реального» прикладу уявіть банк. До недавнього часу більшість банків мали кілька касирів 👨‍💼👨‍💼👨‍💼👨‍💼 і велику чергу 🕙🕙🕙🕙🕙🕙🕙🕙. Усі касири робили всю роботу з одним клієнтом за іншим 👨‍💼⏯. І вам доводилося 🕙 довго стояти в черзі, інакше ви втратите свою чергу. Ви, напевно, не хотіли б брати свою симпатію 😍 із собою у справи до банку 🏦. ### Висновок про бургери { #burger-conclusion } У цьому сценарії «фастфуд із вашою симпатією», оскільки є багато очікування 🕙, значно доцільніше мати рівночасну систему ⏸🔀⏯. Так є у більшості вебзастосунків. Багато-багато користувачів, але ваш сервер чекає 🕙 на їхнє не надто гарне з’єднання, щоб вони надіслали свої запити. А потім знову чекає 🕙 на повернення відповідей. Це «очікування» 🕙 вимірюється у мікросекундах, але все ж, у сумі - це багато очікування в підсумку. Ось чому дуже логічно використовувати асинхронний ⏸🔀⏯ код для веб API. Такий тип асинхронності зробив NodeJS популярним (хоча NodeJS не є паралельним), і це сила Go як мови програмування. І такий самий рівень продуктивності ви отримуєте з **FastAPI**. А оскільки можна мати паралелізм і асинхронність одночасно, ви отримуєте вищу продуктивність, ніж більшість протестованих фреймворків NodeJS, і на рівні з Go, який є компільованою мовою, ближчою до C (усе завдяки Starlette). ### Чи краща рівночасність за паралелізм? { #is-concurrency-better-than-parallelism } Ні! Це не мораль історії. Рівночасність відрізняється від паралелізму. І вона краща у конкретних сценаріях, що містять багато очікування. Через це зазвичай вона значно краща за паралелізм для розробки вебзастосунків. Але не для всього. Щоб урівноважити це, уявімо коротку історію: > Ви маєте прибрати великий брудний будинок. *Так, це вся історія*. --- Тут немає очікування 🕙 - просто багато роботи, яку треба зробити, у багатьох місцях будинку. У вас могли б бути «черги» як у прикладі з бургерами: спочатку вітальня, потім кухня. Але оскільки ви ні на що не чекаєте 🕙, а просто прибираєте, «черги» нічого не змінять. Завершення займе той самий час із «чергами» чи без (рівночасність), і ви виконаєте той самий обсяг роботи. Але в цьому випадку, якби ви могли привести 8 колишніх касирів/кухарів/тепер прибиральників, і кожен з них (разом із вами) взяв би свою зону будинку для прибирання, ви могли б виконати всю роботу паралельно — з додатковою допомогою — і завершити значно швидше. У цьому сценарії кожен з прибиральників (включно з вами) був би процесором, що виконує свою частину роботи. І оскільки більшість часу виконання займає реальна робота (а не очікування), а роботу на комп’ютері виконує CPU, ці проблеми називають «CPU bound». --- Поширені приклади «CPU bound» операцій - це речі, що потребують складної математичної обробки. Наприклад: - **Обробка аудіо** або **зображень**. - **Комп’ютерний зір**: зображення складається з мільйонів пікселів, кожен піксель має 3 значення/кольори, обробка зазвичай потребує обчислення чогось над цими пікселями, усіма одночасно. - **Машинне навчання**: зазвичай потребує великої кількості множень «матриць» і «векторів». Уявіть величезну таблицю з числами і множення всіх їх разом одночасно. - **Глибоке навчання**: це підгалузь машинного навчання, тож те саме застосовується. Просто тут не одна таблиця чисел для множення, а величезний їх набір, і в багатьох випадках ви використовуєте спеціальний процесор для побудови та/або використання цих моделей. ### Рівночасність + паралелізм: веб + машинне навчання { #concurrency-parallelism-web-machine-learning } З **FastAPI** ви можете скористатися рівночасністю, що дуже поширена у веброзробці (та ж головна принада NodeJS). Але ви також можете використати переваги паралелізму і багатопроцесорності (наявність кількох процесів, що працюють паралельно) для навантажень «CPU bound», як у системах машинного навчання. Це, плюс простий факт, що Python є основною мовою для **Data Science**, машинного навчання і особливо глибокого навчання, робить FastAPI дуже вдалим вибором для веб API та застосунків Data Science / машинного навчання (серед багатьох інших). Щоб побачити, як досягти цього паралелізму у продакшні, див. розділ про [Розгортання](deployment/index.md){.internal-link target=_blank}. ## `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, ви також можете це зробити. ### Пишемо свій власний async-код { #write-your-own-async-code } Starlette (і **FastAPI**) базуються на AnyIO, що робить їх сумісними як зі стандартною бібліотекою Python asyncio, так і з Trio. Зокрема, ви можете безпосередньо використовувати AnyIO для ваших просунутих сценаріїв рівночасності, що потребують складніших патернів у вашому коді. І навіть якщо ви не використовували FastAPI, ви могли б писати свої власні async-застосунки з AnyIO, щоб мати високу сумісність і отримати його переваги (наприклад, *структурована рівночасність*). Я створив іншу бібліотеку поверх AnyIO, як тонкий шар, щоб дещо покращити анотації типів і отримати кращу **автодопомогу** (autocompletion), **вбудовані помилки** (inline errors) тощо. Вона також має дружній вступ і навчальний посібник, щоб допомогти вам **зрозуміти** і написати **власний async-код**: Asyncer. Вона буде особливо корисною, якщо вам потрібно **поєднувати async-код зі звичайним** (блокуючим/синхронним) кодом. ### Інші форми асинхронного коду { #other-forms-of-asynchronous-code } Такий стиль використання `async` і `await` відносно новий у мові. Але він значно полегшує роботу з асинхронним кодом. Такий самий (або майже ідентичний) синтаксис нещодавно з’явився в сучасних версіях JavaScript (у Browser і NodeJS). До цього робота з асинхронним кодом була значно складнішою. У попередніх версіях Python ви могли використовувати потоки (threads) або Gevent. Але код набагато складніший для розуміння, налагодження і мислення про нього. У попередніх версіях NodeJS/Browser JavaScript ви б використовували «callbacks», що призводить до «callback hell». ## Співпрограми { #coroutines } **Співпрограма** - це просто дуже вишукана назва для об’єкта, який повертає функція `async def`. Python знає, що це щось на кшталт функції, яку можна запустити і яка завершиться в певний момент, але яку також можна поставити на паузу ⏸ всередині, коли є `await`. Але всю цю функціональність використання асинхронного коду з `async` і `await` часто підсумовують як використання «співпрограм». Це порівняно з головною ключовою особливістю Go - «Goroutines». ## Висновок { #conclusion } Погляньмо на ту саму фразу ще раз: > Сучасні версії Python мають підтримку «асинхронного коду» за допомогою так званих «співпрограм», з синтаксисом **`async` і `await`**. Тепер це має більше сенсу. ✨ Усе це приводить у дію FastAPI (через Starlette) і дає йому таку вражаючу продуктивність. ## Дуже технічні деталі { #very-technical-details } /// warning | Попередження Ймовірно, ви можете пропустити це. Це дуже технічні деталі про те, як **FastAPI** працює «під капотом». Якщо у вас є чимало технічних знань (співпрограми, потоки, блокування тощо) і вам цікаво, як FastAPI обробляє `async def` проти звичайного `def`, - вперед. /// ### Функції операції шляху { #path-operation-functions } Коли ви оголошуєте функцію операції шляху зі звичайним `def` замість `async def`, вона виконується у зовнішньому пулі потоків (threadpool), який потім «очікується», замість прямого виклику (оскільки прямий виклик блокував би сервер). Якщо ви прийшли з іншого async-фреймворку, який не працює так, як описано вище, і звикли визначати тривіальні, лише обчислювальні функції операції шляху зі звичайним `def` заради крихітного виграшу у продуктивності (близько 100 наносекунд), зверніть увагу, що у **FastAPI** ефект буде протилежним. У таких випадках краще використовувати `async def`, якщо тільки ваші функції операції шляху не використовують код, що виконує блокуюче I/O. Втім, у будь-якій ситуації є велика ймовірність, що **FastAPI** [все одно буде швидшим](index.md#performance){.internal-link target=_blank} (або принаймні порівнянним) за ваш попередній фреймворк. ### Залежності { #dependencies } Те саме стосується і [залежностей](tutorial/dependencies/index.md){.internal-link target=_blank}. Якщо залежність є стандартною функцією `def` замість `async def`, вона виконується у зовнішньому пулі потоків. ### Підзалежності { #sub-dependencies } Ви можете мати кілька залежностей і [підзалежностей](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank}, які вимагають одна одну (як параметри визначень функцій). Деякі з них можуть бути створені з `async def`, а деякі - зі звичайним `def`. Все працюватиме, і ті, що створені зі звичайним `def`, будуть викликані у зовнішньому потоці (з пулу потоків), а не «очікувані». ### Інші допоміжні функції { #other-utility-functions } Будь-яка інша допоміжна функція, яку ви викликаєте безпосередньо, може бути створена зі звичайним `def` або `async def`, і FastAPI не впливатиме на спосіб її виклику. Це відрізняється від функцій, які FastAPI викликає за вас: функції операції шляху і залежності. Якщо ваша допоміжна функція є звичайною функцією з `def`, її буде викликано безпосередньо (як ви написали у своєму коді), не в пулі потоків; якщо функція створена з `async def`, тоді вам слід використовувати `await` при її виклику у вашому коді. --- Знову ж таки, це дуже технічні деталі, які, ймовірно, стануть у пригоді, якщо ви спеціально їх шукали. Інакше вам вистачить настанов із розділу вище: Поспішаєте?.