Po asyncio sięgamy zwykle wtedy, gdy chcemy przyspieszyć operacje I/O:
zapytania HTTP, dostęp do bazy danych, pracę z plikami. Główny zysk polega na tym, że gdy
jedno zadanie czeka na odpowiedź, event loop może obsługiwać inne.
W praktyce jednak często zdarza się, że po"przepisaniu na asyncio” API staje się wolniejsze, a zużycie zasobów serwera rośnie. To zwykle nie jest wina samego asyncio, tylko sposobu, w jaki łączymy je z istniejącym kodem i infrastrukturą. Poniżej zebrane są najczęstsze kierunki diagnozy.
I. Blokujące operacje w środku kodu asynchronicznego
Warunkiem skuteczności asyncio jest to, żeby operacje I/O oddawały sterowanie (tzw. yield). Event loop jest jednowątkowy – jeżeli w środku korutyny wywołasz klasyczną, blokującą funkcję, zatrzymasz cały loop.
Typowe wpadki:
time.sleep()użyte"dla testu” wasync def,- zapytania HTTP przez
requestszamiastaiohttp/httpx, - wywołania klasycznych driverów DB (np.
pymysql,psycopg2) bezpośrednio z korutyny.
import asyncio
import time
async def fetch_data_blocking():
# Zły przykład: blokuje event loop na 2 sekundy
time.sleep(2)
return {"status": "ok"}
async def fetch_data_async():
# Dobry przykład: oddaje sterowanie do event loopa
await asyncio.sleep(2)
return {"status": "ok"}
Jeśli z różnych powodów musisz korzystać z biblioteki synchronicznej, rozważ uruchamianie
jej w osobnym wątku/procesie (np. przez asyncio.to_thread() lub
run_in_executor()), tak aby nie zatrzymywać głównego event loopa.
II."Pseudo‑asynchroniczność”: kod nadal wykonuje się sekwencyjnie
Samo dodanie async/await nie oznacza jeszcze współbieżności.
Jeżeli wywołujesz kilka niezależnych operacji I/O w kolejnych liniach z osobnymi
await, to de facto dalej robisz to po kolei.
# Zły przykład: zadania wykonają się jedno po drugim
async def process_sequential():
r1 = await fetch_task1()
r2 = await fetch_task2()
return r1, r2
# Dobry przykład: fetch_task1 i fetch_task2 lecą współbieżnie
async def process_concurrent():
r1, r2 = await asyncio.gather(
fetch_task1(),
fetch_task2(),
)
return r1, r2
W diagnostyce zacznij od przejrzenia miejsc, gdzie masz kilka kolejnych
await. Jeśli dane nie zależą od siebie, to kandydaci do
gather() lub create_task().
III. Zadania CPU‑intensywne w korutynach
asyncio rozwiązuje problem czekania na I/O – nie przyspiesza
ciężkich obliczeń. Jeżeli w korutynach wykonujesz np. serializację ogromnych JSON-ów
lub inne CPU‑intensywne operacje, event loop nie będzie miał kiedy przełączyć się
na pozostałe zadania.
W takich przypadkach warto wydzielić obliczenia do procesów (np. multiprocessing)
albo przynajmniej używać asyncio.to_thread(), by przerzucić je poza główny loop.
IV. Locki, N+1 i inne problemy architektoniczne przy wysokiej współbieżności
Przy rosnącej liczbie zapytań nawet niewielkie braki w architekturze stają się poważnym hamulcem.
- Konflikty na blokadach (lockach) – np. wiele korutyn jednocześnie walczy o ten sam lock w Redisie i agresywnie powtarza próby (polling), co generuje duży ruch i blokuje inne operacje.
- Problem N+1 zapytań – pętla, w której dla każdego elementu robisz osobne wywołanie API/DB. Asynchroniczność może tylko zakamuflować problem, ale go nie usuwa – warto przeprojektować to na zapytania batchowe.
-
Głębokie paginowanie bez indeksów – zapytania SQL typu
LIMIT ... OFFSET ...na dużych tabelach, bez odpowiednich indeksów, będą wolne niezależnie od tego, czy uruchamia je kod sync czy async.
V. Problemy konfiguracyjne i zależności zewnętrzne
Część spadków wydajności nie wynika z samego Pythona, ale z konfiguracji usług, z którymi nasz kod rozmawia.
-
Brak timeoutów – zapytanie bez limitu czasu potrafi wisieć
bardzo długo, zajmując gniazdo sieciowe i miejsce w pulach połączeń.
W asyncio można użyć
asyncio.wait_for()lub wbudowanych timeoutów klientów HTTP/DB. - Złe parametry puli wątków – jeśli część pracy musi trafić do ThreadPool/ProcessPool, zbyt mała liczba wątków tworzy kolejkę zadań, a zbyt duża powoduje nadmierny koszt przełączania kontekstu.
- Ograniczenia po stronie DB / zewnętrznych API – zbyt wiele równoległych zapytań może trafić w limity rate‑limit lub saturację zasobów serwera bazodanowego.
VI. Trzy rzeczy do sprawdzenia przed głębszą analizą
-
Czy biblioteki naprawdę wspierają async?
Nigdy nie wywołuj synchronicznych ORM‑ów anirequestsz wnętrzaasync def. To najczęstszy i najbardziej kosztowny błąd. -
Czy niezależne wywołania I/O są opakowane w
gather()?
Jeżeli dwa zapytania nie muszą czekać na siebie nawzajem, nie powinny stać jedno za drugim w kodzie – przejrzyj sekwencjeawait. -
Czy wszystkie zewnętrzne wywołania mają timeout?
Brak limitu czasu sprawia, że pojedyncze"zawieszone” połączenie może stopniowo wyczerpać zasoby procesu, a ostatecznie nawet całego hosta.