Asynchronní programování je zejména vhodné pro webové API a databázové aplikace. U nich je možné dosáhnout podstatného zredukování výpočetního času.
Náš tým se v Heurece věnuje platebnímu, fakturačnímu a administračnímu systému pro obchody registrované na naší platformě. Všechny tyto systémy píšeme v programovacím jazyce Python.
Asynchronní kód je v našich službách všudypřítomný – kde ho bylo možné přidat, tam jsme ho přidali. Ale takto jsme občas přidali spoustu nepřehledného kódu navíc, který by byl stejně účinný jako ten synchronní. Díky tomu jsme se dostali k otázce, jakým způsobem použít asynchronní programování k optimalizaci našich služeb.
Co je synchronní?
Laickým příkladem může být fronta v obchodě. Zákazník se postaví na konec fronty a na řadu se dostane až poté, co budou odbaveni zákazníci stojící před ním.
Čas strávený ve frontě je přímo úměrný počtu zákazníků před vámi (a náladě obsluhy).
Co je asynchronní?
Asynchronní přístup se na druhou stranu dá demonstrovat na výdejním místě e‑shopu. Zákazníci přicházejí k výdejnímu okénku s objednávkami. Obsluha převezme objednávku a předá ji skladníkům a nadále přijímá objednávky od dalších zákazníků. Mezitím skladníci doručují zboží obsluze a ta ji předává zákazníkovi. Tím dojde k tomu, že je obsluhováno více zákazníků najednou.
V tomto případě je čas strávený v obchodě úměrný rychlosti vybavení skladové dodávky.
Asyncio: o co přesně jde
Knihovna asyncio umožňuje práci s asynchronními funkcemi; jejich spouštění, paralelní spouštění (concurency), rozdělení běhu do jednotlivých vláken procesoru atd.
Pro vytvoření asynchronní funkce je zapotřebí ji definovat klíčovým slovem async, například:
async def fn(): return "Hello from async"
Volání asynchronní funkce provádíme pomocí klíčového slova await. Pokud Python narazí na await, pozastaví provádění hlavní funkce a vyčká na odpověď.
message = await fn()
> Pokud dojde k zavolání async funkce bez klíčového slova await, dojde k výjimce:
> RuntimeWarning: coroutine 'fn' was never awaited
Pro spuštění asynchronního kódu je třeba zavolat metodu run.
asyncio.run(fn())
Sync a async v praxi
Základy
Nejdříve si ukážeme na jednoduchém příkladu, jak vypadá pozastavení vykonávání procesu. Dá se tak simulovat zpoždění odpovědí z API nebo databáze.
Synchronní:
import time def main(): print("Hello") time.sleep(2) print("World!") main() Hello World! Synchronní funkce "main" zabrala 2.0s
Asynchronní:
import asyncio async def main(): print("Hello") await asyncio.sleep(2) print("World!") asyncio.run(main()) Hello World! Asynchronní funkce "main" zabrala 2.0s
V obou případech trvá vykonání obou funkcí stejnou dobu.
Postupné stahování dat
Pro následující příklady si připravíme funkce, které budou posílat HTTP requesty.
import requests
import aiohttp
def get_user_sync(id: int):
response =
requests.get(f"http://localhost/v1/users/{id}")
return response.json()
async def get_user_async(id: int):
async with aiohttp. ClientSession() as session:
async with
session.get(f"http://localhost/v1/users/{id}") as
response:
return await response.json()
> Knihovny requests a aiohttp se používají pro vytváření requestů do API.
Jsou případy, kdy potřebujeme získat postupně data z více zdrojů.
Synchronní:
def main():
print(get_user_sync(1))
print(get_user_sync(3))
print(get_user_sync(6))
main()
Uživatel [id: 1, jméno: Jan Jelínek]
Uživatel [id: 3, jméno: Jelen]
Uživatel [id: 6, jméno: Over engineer]
Synchronní funkce "main" zabrala 9s
Asynchronní:
import asyncio
async def main():
print(await get_user_async(1))
print(await get_user_async(3))
print(await get_user_async(6))
asyncio.run(main())
Uživatel [id: 1, jméno: Jan Jelínek]
Uživatel [id: 3, jméno: Jelen]
Uživatel [id: 6, jméno: Over engineer]
Asynchronní funkce "main" zabrala 9s
Tento příklad ukazuje, že se obě funkce vykonají za stejný čas. Je to zapříčiněno tím, že obě funkce vyčkávají na jednotlivé odpovědi z get_user_sync
a await get_user_async
.
Na následujících příkladech si předvedeme způsoby, jakými optimalizovat kód pomocí asynchronního přístupu.
import asyncio async def main(): results = await asyncio.gather( get_user_async(1), get_user_async(3), get_user_async(6) ) results = [str(result) for result in results] print("\n".join(results)) asyncio.run(main()) Uživatel [id: 1, jméno: Jan Jelínek] Uživatel [id: 3, jméno: Jelen] Uživatel [id: 6, jméno: Over engineer] Asynchronní funkce "main" zabrala 3s
Nebo
import asyncio
from users import User
async def main():
requests = [get_user_async(1), get_user_async(3),
get_user_async(6)]
results = []
for future in asyncio.as_completed(requests):
result = await future
results += [str(result)]
print("\n".join(results))
asyncio.run(main())
Uživatel [id: 1, jméno: Jan Jelínek]
Uživatel [id: 6, jméno: Over engineer]
Uživatel [id: 3, jméno: Jelen]
Asynchronní funkce "main" zabrala 3s
Kód ukazuje dva základní způsoby, jakými lze vykonávat několik asynchronních úloh paralelně.
- asyncio.gather: Spustí všechny funkce zároveň a čeká na jejich odpovědi. Návratové hodnoty vrátí ve stejné struktuře (list), v jaké byly předány funkci gather.
- asyncio.as_completed: Zpracuje paralelně vstupní funkce a jakmile dostane od jedné z nich návratovou hodnotu, vrátí ji a následně čeká na další odpověď. Oproti funkci gather nejsou data seřazena podle toho, jak byly jejich funkce přidány do pole.
Asynchronní generátory
Generátory jsou objekty, které jsou podtřídou iterátoru.
Generátor vrátí hodnotu, ta se zpracuje a hodnota se vrátí zpět do generátoru. Ten pokračuje dál v iteraci. Návratová hodnota se předává pomocí klíčového slova yield.
Často se generátory používají pro cykly nebo objekty, které musí být po vykonání práce uzavřeny (například při používání PyMySQL, Kafka nebo aiohttp).
Pro cyklické načítání dat z asynchronního generátoru se používá konstrukce async for
.
Příklad asynchronního generátoru a cyklu:
import asyncio from typing import List from users import User async def get_users(ids: List [int]): requests = [] for id in ids: requests += [get_user_async(id)] for future in asyncio.as_completed(requests): yield await future async def main(): async for user in get_users([1, 3, 6]): print(user) asyncio.run(main()) Uživatel [id: 6, jméno: Over engineer] Uživatel [id: 1, jméno: Jan Jelínek] Uživatel [id: 3, jméno: Jelen] Asynchronní funkce "main" zabrala 3s
Vytvoří se jednotlivé funkce a přidají se do asyncio.as_complete
, který vrací odpovědi postupně. Tím tak neblokuje běh zbylého kódu.
Pokud bychom chtěli získat například 100 000 záznamů z databáze, můžetem tímto způsobem rozložit jeden dotaz do více separátních dotazů.
Na druhou stranu není rozumné například poslat statisíce requestů na veřejné API. Mohlo by se stát, že provozovatel API vyhodnotí tento přístup jako DDoS útok.
Zaměřte se na to, jakým způsobem async používáte, aby váš kód nebyl v podstatě synchronní. Hlavním přínosem je totiž to, že aplikace zpracovává instrukce paralelně.
Kdy async použít?
- Při vytváření webů a API
- Komunikace s databázemi
- Komunikace s API
Kdy se asyncu vyhnout?
- Při psaní nenáročných aplikací a scriptů
- Zápisu do souborů
- Pokud se nechcete vztekat u debuggingu