Files
fastapi/docs/uk/docs/async.md
2026-01-11 17:44:10 +00:00

37 KiB
Raw Blame History

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

Деталі про синтаксис async def для функцій операцій шляху та деякі пояснення щодо асинхронного коду, конкурентності й паралелізму.

Поспішаєте

TL;DR:

Якщо ви використовуєте сторонні бібліотеки, які кажуть викликати їх з await, наприклад:

results = await some_library()

Тоді оголошуйте ваші функції операцій шляху через async def, наприклад:

@app.get('/')
async def read_results():
    results = await some_library()
    return results

/// note | Примітка

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

///


Якщо ви використовуєте сторонню бібліотеку, яка взаємодіє з чимось (базою даних, API, файловою системою тощо) і не підтримує використання await (зараз це стосується більшості бібліотек для баз даних), тоді оголошуйте ваші функції операцій шляху звичайно, тобто просто з def, наприклад:

@app.get('/')
def results():
    results = some_library()
    return results

Якщо вашому застосунку (якимось чином) не потрібно спілкуватися ні з чим іншим і чекати на відповідь, використовуйте async def, навіть якщо вам не потрібно використовувати await всередині.


Якщо ви просто не знаєте — використовуйте звичайний def.


Примітка: Ви можете змішувати def і async def у ваших функціях операцій шляху стільки, скільки потрібно, і визначати кожну з них, обираючи найкращий для вас варіант. FastAPI зробить усе правильно.

У будь-якому разі, у всіх наведених ситуаціях FastAPI все одно працюватиме асинхронно й буде надзвичайно швидким.

Але якщо дотримуватися кроків вище, він зможе виконати деякі оптимізації продуктивності.

Технічні деталі

Сучасні версії Python підтримують «асинхронний код» за допомогою того, що називається «coroutines», із синтаксисом async і await.

Розберімо цю фразу по частинах у розділах нижче:

  • Асинхронний код
  • async і await
  • Coroutines

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

Асинхронний код означає, що мова 💬 має спосіб сказати комп’ютеру / програмі 🤖, що в певний момент у коді їй 🤖 доведеться чекати, поки щось інше десь в іншому місці завершиться. Скажімо, це щось інше називається «slow-file» 📝.

Отже, протягом цього часу комп’ютер може піти й виконувати іншу роботу, поки «slow-file» 📝 завершується.

Потім комп’ютер / програма 🤖 повертатиметься щоразу, коли матиме змогу — тому що знову чекає, або коли 🤖 завершить всю роботу, яку мав у той момент. І 🤖 перевірить, чи вже завершилися якісь із завдань, на які він чекав, виконуючи те, що потрібно.

Далі 🤖 бере перше завдання, яке завершилося (скажімо, наш «slow-file» 📝), і продовжує робити те, що потрібно, вже з ним.

Це «очікування чогось іншого» зазвичай стосується операцій I/O, які є відносно «повільними» (порівняно зі швидкістю процесора й оперативної пам’яті), наприклад очікування:

  • даних від клієнта, що надсилаються мережею
  • даних, відправлених вашою програмою, щоб клієнт отримав їх через мережу
  • вмісту файлу на диску, який система має прочитати та передати вашій програмі
  • вмісту, який ваша програма передала системі, щоб записати на диск
  • операції віддаленого API
  • завершення операції з базою даних
  • повернення результатів запиту до бази даних
  • тощо

Оскільки час виконання здебільшого витрачається на очікування операцій I/O, їх називають операціями «I/O bound».

Це називають «асинхронним», бо комп’ютеру / програмі не потрібно бути «синхронізованими» з повільним завданням — чекати точного моменту завершення завдання, нічого не роблячи, щоб забрати результат і продовжити роботу.

Натомість, будучи «асинхронною» системою, після завершення завдання може трохи почекати в черзі (кілька мікросекунд), доки комп’ютер / програма завершить те, що пішов робити, а потім повернеться, забере результати та продовжить роботу з ними.

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

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

Описану вище ідею асинхронного коду інколи також називають «конкурентністю». Вона відрізняється від «паралелізму».

І конкурентність, і паралелізм стосуються того, що «різні речі відбуваються більш-менш одночасно».

Але деталі між конкурентністю та паралелізмом доволі різні.

Щоб побачити різницю, уявіть таку історію про бургери:

Конкурентні бургери

Ви йдете зі своєю симпатією по фастфуд, стоїте в черзі, поки касир приймає замовлення в людей перед вами. 😍

Потім настає ваша черга, ви робите замовлення з 2 дуже вишуканих бургерів для вашої симпатії та для вас. 🍔🍔

Касир щось каже кухарю на кухні, щоб ті знали, що мають приготувати ваші бургери (навіть якщо зараз вони готують для попередніх клієнтів).

Ви платите. 💸

Касир дає вам номер вашої черги.

Поки ви чекаєте, ви разом зі своєю симпатією знаходите столик, сідаєте й довго розмовляєте (бо ваші бургери дуже вишукані й готуються певний час).

Поки ви сидите за столиком зі своєю симпатією та чекаєте на бургери, ви можете витратити цей час, милуючись тим, яка чудова, мила й розумна ваша симпатія 😍.

Під час очікування й розмови зі своєю симпатією, час від часу ви перевіряєте номер, який показує табло на прилавку, щоб побачити, чи вже ваша черга.

І в якийсь момент нарешті стає ваша черга. Ви підходите до прилавка, берете бургери та повертаєтеся до столика.

Ви зі своєю симпатією їсте бургери й гарно проводите час.

/// info | Інформація

Чудові ілюстрації від Ketrina Thompson. 🎨

///


Уявіть, що в цій історії ви — це комп’ютер / програма 🤖.

Поки ви в черзі, ви просто простоюєте 😴, чекаючи своєї черги й не роблячи нічого особливо «продуктивного». Але черга рухається швидко, бо касир лише приймає замовлення (а не готує їх), тож це нормально.

Потім, коли настає ваша черга, ви робите справді «продуктивну» роботу: дивитеся меню, вирішуєте, що хочете, дізнаєтеся вибір вашої симпатії, платите, перевіряєте, що віддаєте правильну купюру чи картку, перевіряєте, що з вас списали правильно, перевіряєте, що в замовленні правильні позиції, тощо.

Але далі, навіть якщо у вас ще немає бургерів, ваша робота з касиром «на паузі» ⏸, бо вам треба чекати 🕙, доки бургери будуть готові.

Та оскільки ви відходите від прилавка й сідаєте за столик з номером своєї черги, ви можете перемкнути 🔀 увагу на свою симпатію й «працювати» ⏯ 🤓 над цим. Тоді ви знову робите щось дуже «продуктивне» — наприклад, фліртуєте зі своєю симпатією 😍.

Потім касир 💁 каже «я закінчив готувати бургери», виставляючи ваш номер на табло, але ви не стрибаєте одразу, щойно номер змінюється на ваш. Ви знаєте, що ніхто не вкраде ваші бургери, бо у вас є ваш номер, а в інших — їхній.

Тож ви чекаєте, поки ваша симпатія закінчить історію (завершить поточну роботу ⏯ / завдання, яке обробляється 🤓), лагідно усміхаєтеся й кажете, що підете по бургери ⏸.

Потім ви йдете до прилавка 🔀, до початкового завдання, яке вже завершене ⏯, забираєте бургери, дякуєте та несете їх до столика. Це завершує цей крок / завдання взаємодії з прилавком ⏹. І, своєю чергою, створює нове завдання — «їсти бургери» 🔀 ⏯, але попереднє «отримати бургери» вже завершене ⏹.

Паралельні бургери

А тепер уявімо, що це не «конкурентні бургери», а «паралельні бургери».

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

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

Кожен перед вами чекає, доки його бургери будуть готові, перш ніж відійти від прилавка, бо кожен із 8 касирів одразу йде готувати бургер, перш ніж взяти наступне замовлення.

Нарешті настає ваша черга, ви робите замовлення з 2 дуже вишуканих бургерів для вашої симпатії та для вас.

Ви платите 💸.

Касир іде на кухню.

Ви чекаєте, стоячи перед прилавком 🕙, щоб ніхто інший не забрав ваші бургери раніше за вас, адже немає номерів черги.

Оскільки ви та ваша симпатія зайняті тим, щоб ніхто не проліз уперед і не забрав ваші бургери, коли вони з’являться, ви не можете приділяти увагу своїй симпатії. 😞

Це «синхронна» робота: ви «синхронізовані» з касиром/кухарем 👨‍🍳. Вам треба чекати 🕙 й бути там у точний момент, коли касир/кухар 👨‍🍳 закінчить бургери й віддасть їх вам, інакше хтось інший може забрати їх.

Потім ваш касир/кухар 👨‍🍳 нарешті повертається з вашими бургерами, після довгого очікування 🕙 там, перед прилавком.

Ви берете бургери й ідете до столика зі своєю симпатією.

Ви просто їсте їх, і все. ⏹

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

/// info | Інформація

Чудові ілюстрації від Ketrina Thompson. 🎨

///


У сценарії з паралельними бургерами ви — комп’ютер / програма 🤖 із двома процесорами (ви та ваша симпатія), обидва чекають 🕙 і утримують увагу ⏯ на «очікуванні біля прилавка» 🕙 протягом тривалого часу.

У закладі фастфуду є 8 процесорів (касири/кухарі). Тоді як у закладі з конкурентними бургерами могло бути лише 2 (один касир і один кухар).

Але все одно, підсумковий досвід не найкращий. 😞


Це була паралельна «бургерна» історія. 🍔

Для більш «життєвого» прикладу уявіть банк.

Ще донедавна в більшості банків було багато касирів 👨‍💼👨‍💼👨‍💼👨‍💼 і велика черга 🕙🕙🕙🕙🕙🕙🕙🕙.

Усі касири виконували всю роботу з одним клієнтом за іншим 👨‍💼⏯.

А вам треба чекати 🕙 у черзі довго, інакше ви втрачаєте свою чергу.

Ймовірно, ви б не хотіли брати свою симпатію 😍 з собою у справах до банку 🏦.

Висновок про бургери

У цьому сценарії «фастфуд із бургерами зі своєю симпатією», оскільки є багато очікування 🕙, значно логічніше мати конкурентну систему ⏸🔀⏯.

Так буває для більшості вебзастосунків.

Дуже багато користувачів, але ваш сервер чекає 🕙, поки їхнє не дуже хороше з’єднання надішле їхні запити.

А потім знову чекає 🕙, поки повернуться відповіді.

Це «очікування» 🕙 вимірюється мікросекундами, але якщо все підсумувати, зрештою це багато часу очікування.

Саме тому має сенс використовувати асинхронний ⏸🔀⏯ код для веб-API.

Саме така асинхронність зробила NodeJS популярним (хоча NodeJS не є паралельним), і це сильна сторона Go як мови програмування.

І це той самий рівень продуктивності, який ви отримуєте з FastAPI.

А оскільки ви можете мати паралелізм і асинхронність одночасно, ви отримуєте вищу продуктивність, ніж у більшості протестованих NodeJS-фреймворків, і рівень, порівняний з Go, який є компільованою мовою, ближчою до C (усе завдяки Starlette).

Чи конкурентність краща за паралелізм

Ні! Це не мораль історії.

Конкурентність відрізняється від паралелізму. І вона краща в певних сценаріях, які включають багато очікування. Через це вона зазвичай значно краща за паралелізм для розробки вебзастосунків. Але не для всього.

Тож, щоб урівноважити це, уявіть таку коротку історію:

Вам треба прибрати великий, брудний будинок.

Так, це вся історія.


Немає жодного очікування 🕙, лише багато роботи, яку треба виконати в різних місцях будинку.

Ви могли б робити «черги», як у прикладі з бургерами: спочатку вітальня, потім кухня, але оскільки ви ні на що не чекаєте 🕙 — лише прибираєте й прибираєте — черговість нічого б не змінила.

Це зайняло б стільки ж часу із «чергами» (конкурентністю) або без них, і ви виконали б ту саму кількість роботи.

Але в цьому випадку, якби ви могли привести 8 колишніх касирів/кухарів/тепер-прибиральників, і кожен із них (плюс ви) взяв би зону будинку, ви могли б виконувати всю роботу паралельно, з додатковою допомогою, і закінчити набагато швидше.

У цьому сценарії кожен прибиральник (включно з вами) був би процесором, що робить свою частину роботи.

І оскільки більшість часу виконання займає реальна робота (а не очікування), а робота в комп’ютері виконується CPU, такі задачі називають «CPU bound».


Поширені приклади CPU bound операцій — це те, що потребує складної математичної обробки.

Наприклад:

  • Обробка аудіо або зображень.
  • Комп’ютерний зір: зображення складається з мільйонів пікселів, кожен піксель має 3 значення/кольори; обробка зазвичай вимагає обчислювати щось над цими пікселями — усіма одночасно.
  • Machine Learning: зазвичай потребує великої кількості множень «матриць» і «векторів». Уявіть величезну електронну таблицю з числами та множення їх усіх одночасно.
  • Deep Learning: це підгалузь Machine Learning, тож діє те саме. Просто це не одна таблиця чисел для множення, а величезний набір таких таблиць, і в багатьох випадках ви використовуєте спеціальний процесор для побудови та/або використання цих моделей.

Конкурентність + паралелізм: веб + Machine Learning

З FastAPI ви можете скористатися конкурентністю, яка дуже типова для веброзробки (це та сама головна перевага NodeJS).

Але ви також можете використовувати переваги паралелізму та multiprocessing (коли кілька процесів працюють паралельно) для CPU bound навантажень, як у системах Machine Learning.

Це, плюс простий факт, що Python — головна мова для Data Science, Machine Learning і особливо Deep Learning, робить FastAPI дуже вдалим вибором для веб-API та застосунків у Data Science / Machine Learning (серед багатьох інших).

Щоб побачити, як досягти цього паралелізму у production, дивіться розділ про Розгортання{.internal-link target=_blank}.

async і await

Сучасні версії Python мають дуже інтуїтивний спосіб визначати асинхронний код. Це робить його схожим на звичайний «послідовний» код і виконує «очікування» за вас у потрібні моменти.

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

burgers = await get_burgers(2)

Ключове тут — await. Він каже Python, що треба зачекати ⏸, доки get_burgers(2) завершить свою справу 🕙, перш ніж зберегти результат у burgers. Завдяки цьому Python знатиме, що тим часом може піти й зробити щось інше 🔀 ⏯ (наприклад, прийняти інший запит).

Щоб await працював, він має бути всередині функції, яка підтримує цю асинхронність. Для цього ви просто оголошуєте її через async def:

async def get_burgers(number: int):
    # Do some asynchronous stuff to create the burgers
    return burgers

...замість def:

# This is not asynchronous
def get_sequential_burgers(number: int):
    # Do some sequential stuff to create the burgers
    return burgers

З async def Python знає, що всередині цієї функції треба враховувати вирази await, і що він може «призупинити» ⏸ виконання цієї функції та піти зробити щось інше 🔀, перш ніж повернутися.

Коли ви хочете викликати функцію async def, вам треба її «await». Тож це не працюватиме:

# This won't work, because get_burgers was defined with: async def
burgers = get_burgers(2)

Отже, якщо ви використовуєте бібліотеку, яка каже, що її можна викликати з await, вам потрібно створювати функції операцій шляху, які її використовують, через async def, як тут:

@app.get('/burgers')
async def read_burgers():
    burgers = await get_burgers(2)
    return burgers

Більш технічні деталі

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

Але водночас функції, визначені через async def, потрібно «await». Тож функції з async def можна викликати лише всередині функцій, визначених через async def.

Тож щодо «курки й яйця»: як викликати першу async-функцію?

Якщо ви працюєте з FastAPI, вам не потрібно про це турбуватися, бо «першою» функцією буде ваша функція операції шляху, і FastAPI знатиме, як зробити все правильно.

Але якщо ви хочете використовувати async / await без FastAPI — ви теж можете.

Пишіть власний async-код

Starlette (і FastAPI) базуються на AnyIO, що робить їх сумісними і зі стандартною бібліотекою Python asyncio, і з Trio.

Зокрема, ви можете безпосередньо використовувати AnyIO для ваших просунутих сценаріїв конкурентності, які потребують складніших шаблонів у власному коді.

І навіть якщо ви не використовуєте FastAPI, ви також можете писати власні async-застосунки з AnyIO, щоб мати високу сумісність і отримати його переваги (наприклад, structured concurrency).

Я створив ще одну бібліотеку поверх AnyIO як тонкий шар, щоб трохи покращити type annotations і отримати кращі autocompletion, inline errors тощо. Вона також має дружній вступ і навчальний посібник, щоб допомогти вам зрозуміти та писати власний async-код: Asyncer. Вона буде особливо корисною, якщо вам потрібно поєднувати async-код зі звичайним (blocking/synchronous) кодом.

Інші форми асинхронного коду

Цей стиль використання async і await є відносно новим у мові.

Але він значно спрощує роботу з асинхронним кодом.

Подібний (або майже ідентичний) синтаксис нещодавно з’явився також у сучасних версіях JavaScript (у Browser і NodeJS).

Але до цього обробка асинхронного коду була значно складнішою.

У попередніх версіях Python ви могли використовувати потоки (threads) або Gevent. Але такий код значно складніше розуміти, налагоджувати й продумувати.

У попередніх версіях NodeJS / Browser JavaScript ви б використовували «callbacks», що призводить до «callback hell».

Coroutines

Coroutine — це просто дуже «пишний» термін для того, що повертає функція async def. Python знає, що це щось на кшталт функції, яку можна запустити й яка колись завершиться, але яка також може бути призупинена ⏸ всередині, коли є await.

Але всю цю функціональність використання асинхронного коду з async і await часто узагальнюють як використання «coroutines». Це можна порівняти з головною фішкою Go — «Goroutines».

Підсумок

Подивімося на ту саму фразу з вище:

Сучасні версії Python підтримують «асинхронний код» за допомогою того, що називається «coroutines», із синтаксисом async і await.

Тепер це має бути зрозумілішим.

Усе це — те, що «живить» FastAPI (через Starlette) і дає йому таку вражаючу продуктивність.

Дуже технічні деталі

/// warning | Попередження

Ймовірно, ви можете пропустити це.

Це дуже технічні деталі того, як FastAPI працює всередині.

Якщо ви маєте достатньо технічних знань (coroutines, threads, blocking тощо) і вам цікаво, як FastAPI обробляє async def проти звичайного def, продовжуйте.

///

Функції операцій шляху

Коли ви оголошуєте функцію операції шляху звичайним def замість async def, вона виконується в зовнішньому threadpool, і вже його «await», замість прямого виклику (бо це заблокувало б сервер).

Якщо ви переходите з іншого async-фреймворку, який не працює так, як описано вище, і звикли визначати тривіальні функції операцій шляху лише з обчисленнями через простий def заради невеликого виграшу продуктивності (приблизно 100 наносекунд), зверніть увагу, що в FastAPI ефект буде протилежним. У таких випадках краще використовувати async def, якщо тільки ваші функції операцій шляху не використовують код, що виконує блокувальні I/O.

Утім, у обох ситуаціях, найімовірніше, FastAPI буде все одно швидшим{.internal-link target=_blank}, ніж (або принаймні порівнянним із) ваш попередній фреймворк.

Залежності

Те саме стосується залежностей{.internal-link target=_blank}. Якщо залежність — це стандартна функція def, а не async def, вона виконується в зовнішньому threadpool.

Підзалежності

Ви можете мати кілька залежностей і підзалежностей{.internal-link target=_blank}, які потребують одна одну (як параметри у визначеннях функцій); деякі з них можуть бути створені через async def, а деякі — через звичайний def. Це все одно працюватиме, а ті, що створені через звичайний def, будуть викликані в зовнішньому потоці (із threadpool), замість того щоб їх «await».

Інші допоміжні функції

Будь-яка інша допоміжна функція, яку ви викликаєте безпосередньо, може бути створена як звичайним def, так і async def, і FastAPI не впливатиме на те, як ви її викликаєте.

Це відрізняється від функцій, які FastAPI викликає за вас: функцій операцій шляху та залежностей.

Якщо ваша допоміжна функція — звичайна з def, вона буде викликана напряму (як ви написали у своєму коді), не в threadpool; якщо функція створена через async def, тоді під час виклику у вашому коді вам слід зробити await.


Знову ж таки, це дуже технічні деталі, які, ймовірно, корисні, якщо ви спеціально їх шукали.

Інакше вам має вистачити рекомендацій із розділу вище: Поспішаєте?.