Python asyncio: asinchroninis programavimas

Kodėl asinchroninis programavimas tapo būtinybe

Prisimenu, kaip prieš kelerius metus pirmą kartą susidūriau su situacija, kai mano Python programa tiesiog „kabojo” laukdama atsakymo iš išorinio API. Vartotojas spaudė mygtuką, o programa atrodė tarsi užšalusi – nieko nevyko, kol nebuvo gautas atsakymas. Tai klasikinė sinchroninio programavimo problema, kuri daugelį programuotojų stumia link asinchroninio požiūrio.

Python asyncio biblioteka atsirado kaip atsakas į šiuos iššūkius. Ji leidžia programai atlikti kitas užduotis, kol laukiama ilgai trunkančių operacijų – tinklo užklausų, failų skaitymo ar duomenų bazės atsakymų. Skirtingai nei tradicinis daugiagijis programavimas (multithreading), asyncio naudoja vieną giją, bet sugeba efektyviai valdyti daugybę užduočių.

Šiandien asinchroninis programavimas nėra vien tik „nice to have” funkcionalumas – tai tampa standartu kuriant modernias web aplikacijas, API serverius ar bet kokias sistemas, kurios intensyviai bendrauja su išoriniais šaltiniais. Jei jūsų programa daro daugiau nei vieną tinklo užklausą vienu metu, asyncio gali sutaupyti ne sekundes, o minutes ar net valandas vykdymo laiko.

Kaip veikia event loop – asinchroninio programavimo širdis

Event loop yra pagrindinis asyncio mechanizmas, kuris koordinuoja visų asinchroninių operacijų vykdymą. Galima įsivaizduoti jį kaip labai efektyvų darbuotoją, kuris nuolat tikrina, ar yra užduočių, kurias galima atlikti dabar, ir perjungia dėmesį tarp jų.

Štai paprastas pavyzdys, kaip tai atrodo praktikoje:


import asyncio

async def fetch_data(name, delay):
print(f"Pradedama užklausa: {name}")
await asyncio.sleep(delay) # Imituoja tinklo užklausą
print(f"Baigta užklausa: {name}")
return f"Duomenys iš {name}"

async def main():
task1 = asyncio.create_task(fetch_data("API-1", 2))
task2 = asyncio.create_task(fetch_data("API-2", 1))
task3 = asyncio.create_task(fetch_data("API-3", 3))

results = await asyncio.gather(task1, task2, task3)
print(results)

asyncio.run(main())

Šis kodas paleis tris „užklausas” vienu metu, ir visa programa užtruks apie 3 sekundes (ilgiausios užklausos laikas), o ne 6 sekundes (visų užklausų suma), kaip būtų sinchroniniame variante.

Event loop veikia pagal paprastą principą: kai funkcija pasiekia await raktažodį, ji „atsipalaiduoja” ir leidžia event loop vykdyti kitas užduotis. Kai laukiama operacija baigiasi, funkcija tęsia darbą nuo tos vietos, kur sustojo.

Async ir await – jūsų nauji geriausi draugai

Raktažodžiai async ir await yra asinchroninio programavimo pagrindas. Pradžioje jie gali atrodyti keistai, bet greitai tampa antra prigimtimi.

async def apibrėžia korutinę (coroutine) – specialią funkciją, kuri gali būti sustabdyta ir vėl tęsiama. Svarbu suprasti, kad tiesiog iškvietus tokią funkciją, ji neiškart vykdoma:


async def greet():
return "Labas"

# Tai nesuspausdins "Labas", o grąžins coroutine objektą
result = greet()
print(result) #

# Teisingas būdas:
result = asyncio.run(greet())
print(result) # Labas

await raktažodis naudojamas tik async funkcijose ir nurodo, kad čia programa gali „palaukti” rezultato, leisdama event loop vykdyti kitas užduotis. Dažniausia klaida pradedantiesiems – bandyti naudoti await ne async funkcijoje arba pamiršti await prieš asinchroninę operaciją.

Praktinis patarimas: jei matote įspėjimą „coroutine was never awaited”, tai reiškia, kad sukūrėte asinchroninę užduotį, bet nepalaukėte jos rezultato su await arba neįtraukėte jos į event loop.

Realūs panaudojimo scenarijai ir pavyzdžiai

Teorija yra gera, bet pažiūrėkime, kaip asyncio sprendžia realias problemas. Vienas dažniausių scenarijų – duomenų rinkimas iš kelių API.

Tarkime, kuriate aplikaciją, kuri turi gauti informaciją apie orą iš trijų skirtingų šaltinių ir palyginti rezultatus:


import asyncio
import aiohttp # Reikia įdiegti: pip install aiohttp

async def fetch_weather(session, city, api_url):
try:
async with session.get(f"{api_url}/{city}") as response:
data = await response.json()
return {"city": city, "data": data, "source": api_url}
except Exception as e:
return {"city": city, "error": str(e), "source": api_url}

async def get_weather_comparison(city):
apis = [
"https://api.weather1.com",
"https://api.weather2.com",
"https://api.weather3.com"
]

async with aiohttp.ClientSession() as session:
tasks = [fetch_weather(session, city, api) for api in apis]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results

# Naudojimas
weather_data = asyncio.run(get_weather_comparison("Vilnius"))

Šis kodas vykdys tris API užklausas lygiagrečiai. Jei kiekviena užklausa užtrunka 1 sekundę, visa operacija užtruks ~1 sekundę, o ne 3.

Kitas populiarus scenarijus – web scraping. Kai reikia aplankyti šimtus puslapių, asinchroninis požiūris gali būti 10-20 kartų greitesnis:


async def scrape_page(session, url):
async with session.get(url) as response:
html = await response.text()
# Čia būtų jūsų scraping logika
return len(html)

async def scrape_multiple_sites(urls):
async with aiohttp.ClientSession() as session:
tasks = [scrape_page(session, url) for url in urls]
results = await asyncio.gather(*tasks)
return sum(results)

Įprastos klaidos ir kaip jų išvengti

Per metus dirbant su asyncio, susidūriau su daugybe klaidų, kurios dabar atrodo akivaizdžios, bet pradžioje sukėlė nemažai galvos skausmo.

Klaida #1: Blokuojančios operacijos async funkcijose

Didžiausia klaida – naudoti įprastas blokuojančias operacijas async funkcijose:


# BLOGAI
async def bad_example():
response = requests.get("https://api.example.com") # Blokuoja!
time.sleep(5) # Blokuoja!
return response.json()

# GERAI
async def good_example():
async with aiohttp.ClientSession() as session:
async with session.get("https://api.example.com") as response:
await asyncio.sleep(5) # Neblokuoja
return await response.json()

Jei naudojate įprastą requests biblioteką ar time.sleep(), jūs blokuojate visą event loop. Naudokite asinchronines alternatyvas: aiohttp vietoj requests, asyncio.sleep() vietoj time.sleep().

Klaida #2: Neapdorotos išimtys

Asyncio išimtys gali būti klastingos. Jei task’as meta išimtį, bet jūs jos neapdorojate, programa gali tyliai tęsti darbą:


async def risky_operation():
raise ValueError("Kažkas nutiko!")

async def main():
task = asyncio.create_task(risky_operation())
# Jei nepalauksite task'o, išimtis gali būti praleista
await asyncio.sleep(1)
# Geriau:
try:
await task
except ValueError as e:
print(f"Pagauta klaida: {e}")

Klaida #3: Per daug lygiagrečių operacijų

Nors asyncio leidžia vykdyti tūkstančius operacijų vienu metu, tai nereiškia, kad turėtumėte. Daugelis serverių turi rate limiting, o jūsų sistema – ribotus resursus:


async def controlled_execution(urls, max_concurrent=10):
semaphore = asyncio.Semaphore(max_concurrent)

async def fetch_with_limit(url):
async with semaphore:
return await fetch_url(url)

tasks = [fetch_with_limit(url) for url in urls]
return await asyncio.gather(*tasks)

Semaphore leidžia kontroliuoti, kiek operacijų vyksta vienu metu, apsaugodamas nuo serverio perkrovimo ar IP blokavimo.

Asyncio ir tradicinis multithreading – kada ką naudoti

Vienas dažniausių klausimų: kodėl naudoti asyncio, kai yra threading ar multiprocessing? Atsakymas priklauso nuo jūsų užduoties pobūdžio.

Asyncio tinka, kai:
– Daug I/O operacijų (tinklo užklausos, failų skaitymas)
– Reikia valdyti tūkstančius vienu metu vykdomų operacijų
– Operacijos daugiausia „laukia” (await) rezultatų
– Norite paprastesnio kodo nei su threading

Threading/Multiprocessing tinka, kai:
– CPU intensyvios operacijos (skaičiavimai, duomenų apdorojimas)
– Naudojate bibliotekas, kurios neturi async palaikymo
– Reikia tikro lygiagretaus vykdymo keliuose procesorių branduoliuose

Praktikoje dažnai naudoju hibridinį požiūrį:


import asyncio
from concurrent.futures import ProcessPoolExecutor

def cpu_intensive_task(data):
# Sunkūs skaičiavimai
return sum(i**2 for i in range(data))

async def hybrid_approach(data_list):
loop = asyncio.get_event_loop()
with ProcessPoolExecutor() as executor:
tasks = [
loop.run_in_executor(executor, cpu_intensive_task, data)
for data in data_list
]
results = await asyncio.gather(*tasks)
return results

Šis kodas naudoja asyncio koordinavimui, bet CPU intensyvias užduotis vykdo atskiruose procesuose.

Debugging ir testavimas – kaip nesustingti

Asinchroninio kodo derinimas gali būti iššūkis, ypač kai klaidos atsiranda tik tam tikromis sąlygomis arba race condition situacijose.

Naudingiausi įrankiai ir technikos:

1. Asyncio debug režimas


import asyncio
import warnings

# Įjunkite debug režimą
asyncio.run(main(), debug=True)

# Arba per environment kintamąjį
# PYTHONASYNCIODEBUG=1 python script.py

Debug režimas perspės apie pamirštas await operacijas, per ilgai vykdomas užduotis ir kitas problemas.

2. Logging su kontekstu


import logging
import asyncio

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

async def logged_operation(name):
logger.debug(f"[{name}] Pradedama operacija")
await asyncio.sleep(1)
logger.debug(f"[{name}] Baigta operacija")

3. Testavimas su pytest-asyncio


# pip install pytest-asyncio

import pytest

@pytest.mark.asyncio
async def test_async_function():
result = await my_async_function()
assert result == expected_value

@pytest.mark.asyncio
async def test_with_mock():
with patch('module.async_function') as mock:
mock.return_value = asyncio.coroutine(lambda: "mocked")()
result = await function_under_test()
assert result == "expected"

Dar vienas patarimas – naudokite asyncio.wait_for() su timeout, kad išvengtumėte begalinio laukimo testuose:


async def test_with_timeout():
try:
result = await asyncio.wait_for(slow_operation(), timeout=5.0)
except asyncio.TimeoutError:
pytest.fail("Operacija užtruko per ilgai")

Kas toliau – asyncio ateitis ir praktiniai patarimai

Python asyncio ekosistema nuolat auga. Vis daugiau bibliotekų prideda asinchroninį palaikymą – nuo duomenų bazių driverių (asyncpg, motor) iki web framework’ų (FastAPI, aiohttp). Jei planuojate rimtai dirbti su asyncio, verta sekti šias tendencijas.

Keletas praktinių patarimų iš asmeninės patirties:

Pradėkite paprastai. Nereikia iškart perrašyti visos aplikacijos į async. Pradėkite nuo vienos funkcijos ar modulio, kur asinchroninis požiūris duos didžiausią naudą – pavyzdžiui, API klientas ar duomenų rinkimo scriptas.

Naudokite type hints. Asyncio kodas su type hints yra daug lengviau skaitomas ir prižiūrimas:


from typing import List, Dict, Any

async def fetch_users(user_ids: List[int]) -> Dict[int, Any]:
# Jūsų kodas čia
pass

Stebėkite performance. Asyncio nėra magiškas greičio sprendimas. Naudokite profiling įrankius kaip aiomonitor ar aiodebug, kad suprastumėte, kur jūsų programa leidžia laiką.

Dokumentuokite asinchroninį elgesį. Būsimasis jūs (ar jūsų kolegos) padėkos už aiškius komentarus apie tai, kokios operacijos yra asinchroninės ir kodėl:


async def process_orders(orders: List[Order]) -> List[Result]:
"""
Apdoroja užsakymus asinchroniškai.

Kiekvienas užsakymas reikalauja:
- API užklausos į mokėjimo sistemą (~500ms)
- Duomenų bazės įrašo (~100ms)
- Email siuntimo (~300ms)

Su asyncio visa operacija užtrunka ~500ms vietoj 900ms per užsakymą.
"""
# Implementacija

Asyncio nėra sudėtinga, kai suprantate pagrindinius principus. Taip, pradžioje gali tekti pergalvoti, kaip struktūruojate kodą, bet rezultatas – greitesnės, efektyvesnės aplikacijos – tikrai to verta. Nebijokite eksperimentuoti, darykite klaidas kontroliuojamoje aplinkoje ir greitai pastebėsite, kad asinchroninis programavimas tampa natūralia jūsų įrankių dalimi. O kai pirmą kartą pamatysite, kaip jūsų programa apdoroja šimtus užklausų per sekundes vietoj keliolikos, suprasite, kodėl asyncio tapo tokia svarbia Python ekosistemos dalimi.

Daugiau

Payload CMS: TypeScript headless sistema