Vývoj

Sync versus Async v Pythonu

Jan Jelínek

Vývoj

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

Autor článku

Jelen je hrdým členem týmu One B2B Payment a věnuje se vývoji platebního systému. Věří, že je důležité rozvíjet vlastní potenciál a být lepší verzí sebe sama. Jeho koníčky jsou metalová hudba, hudební festivaly a posilování.

Podobné články

Ikony bez kompromisů

Ikony bez kompromisů

I přes svou malou velikost představují ikony na webu zajímavý problém. Jeden přístup střídá další –…

Vánoční resuscitace serverů

Vánoční resuscitace serverů

O sysadminech v Heurece se dá říci leccos, nedostatek paranoie to ale není. Máme zdvojené téměř…

Potkejme se na WebExpu!

Potkejme se na WebExpu!

Letošní ročník konference WebExpo 2018, točící se kolem webových technologií, obohatíme i naším…

Zaber si svou židli!

<Nejsme asociálové/>

<Témata/>

Zajímá tě naše práce, technologie, tým nebo cokoliv jiného?
Napiš šéfovi vývoje Lukášovi Putnovi.

lukas.putna@heureka.cz