Přepínání vláken je čistě v režii interpretu. Python 3 přepíná vlákna automaticky po intervalu 5 ms (hodnota sys.getswitchinterval
) nehledě na to, co se ve vláknu děje a jestli je to potřeba. Přepínání kontextu samozřejmě zabere nějaký čas a navíc nedokážeme zajistit, při které konkrétní instrukci k přepnutí dojde. Bylo by proto ideální přepínat úlohy pouze ve chvíli, kdy je to potřeba. Tady se dostáváme k pojmu coroutines.
Všichni známe subroutines, což jsou naše standardní Python funkce, které se zpracovávají řádek po řádku a nakonec vrátí nějakou odpověď. Coroutines jsou vlastně kooperativní subroutines. Taky se zpracovávají řádek po řádku, ale pomocí klíčového slova await jsme schopni zpracovávání takové funkce pozastavit a předat výpočetní čas jiné coroutine. Toto “zapauzování” se hodí všude tam, kde děláme nějakou I/O-bound operaci, jako komunikace po síti nebo se souborovým systémem. Když čekáme například na odpověď databáze, může náš procesor dělat něco víc efektivního, než jen na ni čekat.
AsyncIO
AsyncIO je concurrency design představený v Pythonu 3.4 pomocí dekorátorů asyncio.coroutine
postavený nad generátory. Od Pythonu 3.5 se objevily nativní klíčová slova async
/await
. Aktuálně je přístup pomocí generátorů deprecated a od 3.11 bude odstraněn.
@asyncio.coroutine
def py34_coro():
"""Stara syntaxe"""
yield from stuff()
async def py35_coro():
"""Nativni, moderni syntaxe"""
await stuff()
Základem AsyncIO jsou coroutines a event loop. Event loop je smyčka, která se stará o kontrolu a spouštění dílčích úloh. Celé AsyncIO si můžeme představit tak, že máme k dispozici seznam úloh, ze kterých se jedna vybere a spustí. Pokud interpretr narazí na klíčové slovo await, předpokládá se, že se čeká na nějakou odpověď a mezi tím se vrátí kontrola zpátky hlavní smyčce. Ta opět vybere nějakou úlohu a tu spustí.
Praktická ukázka
V následujícím "hello world" příkladu si ukážeme, jakým způsobem můžeme v asyncio přístupu stahovat data z více url souběžně. Z xkcd api bychom rádi stáhli metadata k náhodným komiksovým stripům a na standardní výstup potom vypsali jejich alternativní popisky (alt
atribut).
import asyncio
import httpx
import random
async def download_url(url):
print(f"Downloading url {url}")
async with httpx.AsyncClient() as client:
response = await client.get(url)
print(f"Url {url} downloaded")
return response.json()["alt"]
async def main():
data = await asyncio.gather(
download_url(f"https://xkcd.com/{random.randint(0, 600)}/info.0.json"),
download_url(f"https://xkcd.com/{random.randint(0, 600)}/info.0.json"),
download_url(f"https://xkcd.com/{random.randint(0, 600)}/info.0.json")
)
print(data)
if __name__ == "__main__":
asyncio.run(main())
Vyrobíme si funkci download_url
, která bude náhodnou url pomocí knihovny httpx stahovat, data z jsonu převede do slovníku a vrátí samotný alt.
💡 Pozor, pokud se jednou rozhodnete psát svůj kód asynchronně, používejte asynchronní knihovny a neblokující volání všude, kde to jde. Jedno nešťastné blokující volání vám může zhatit všechny snahy o urychlení aplikace.
Po spuštění aplikace se zavolá asyncio.run()
a vyrobí se nová smyčka událostí (event loop), do které se přidá jedna úloha (main()
). Ve funkci main()
pak voláme asyncio.gather()
, což nám v tomto případě do smyčky zařadí další 3 úlohy. Tady si můžete všimnout, že zavoláním funkce download_url()
se taková funkce přímo nespustí, ale vyrobí se objekt typu "coroutine
". Aby se coroutine spustila, musí před ní být klíčové slovíčko await
. Když se teď zaměříme na funkci download_url
, tak po jejím spuštění se na standardní výstup vypíše hláška, že se začínají stahovat data a pak následuje async/await
blok, ve kterém se provolá cílová url. V tuto chvíli se vrátí řízení zpět do smyčky událostí a ta začne zpracovávat další úlohu v pořadí, což je pravděpodobně další download_url()
coroutine. Když z cílové url přijdou data, coroutine se "odpauzuje" a její zpracování probíhá standardně řádek po řádku až do samotného konce.