Przy pracy z I/O – HTTP, bazami danych, kolejkami – Python oferuje kilka sposobów na współbieżność. Dla wielu osób zaczynających przygodę z nowoczesnym Pythonem (FastAPI, asyncio) wybór między wątkami, procesami a korutynami bywa trudny.
W skrócie:
- Multiprocessing omija GIL i jest dobry dla zadań CPU‑intensywnych, ale ma wysoki koszt uruchomienia i komunikacji.
- Threading sprawdza się przy I/O, ale nadal podlega ograniczeniom systemowych wątków i GIL‑owi.
- Korutyny (asyncio) działają w jednym wątku, są bardzo lekkie pamięciowo i świetnie nadają się do obsługi tysięcy połączeń sieciowych.
W tym tekście skupimy się na asyncio: jak działa event loop, jak definiować korutyny
oraz jak zarządzać wieloma zadaniami za pomocą gather i
as_completed.
I. Jak działa event loop?
Event loop możesz traktować jak jednego kucharza w restauracji: zamówienia to zadania, a piekarnik czy mikrofalówka to operacje I/O. Kucharz wkłada danie do piekarnika (wysyła zapytanie HTTP), ustawia timer i nie czeka przy drzwiczkach – wraca do innych zamówień. Gdy piekarnik"dingnie”, wraca do tego konkretnego dania.
W kodzie tę pauzę reprezentuje await. Zatrzymuje ono bieżącą korutynę
i oddaje sterowanie do event loopa, który zajmuje się pozostałymi zadaniami.
II. Definiowanie i uruchamianie podstawowej korutyny
Funkcja staje się korutyną, gdy dodasz przed nią słowo kluczowe async.
Jej wywołanie nie wykonuje kodu od razu, tylko zwraca obiekt korutyny.
import asyncio
async def fetch_data(task_id: int, delay: float) -> str:
print(f"Zadanie {task_id} startuje...")
await asyncio.sleep(delay) # symulacja I/O
return f"Zadanie {task_id} zakończone"
async def main():
result = await fetch_data(1, 1.5)
print(result)
if __name__ == "__main__":
asyncio.run(main())
fetch_data jest korutyną – nie można jej"po prostu wywołać”.
Musi zostać uruchomiona z poziomu innej korutyny za pomocą await
albo przez asyncio.run() jako główny punkt wejścia programu.
III. Zarządzanie wieloma zadaniami: gather vs as_completed
W realnych systemach zwykle nie pobieramy jednej rzeczy na raz, tylko wiele – np.
kilku obrazów, kilku API albo wielu rekordów z bazy. Czyste
for ... await jedno po drugim kładzie współbieżność. Tu z pomocą
przychodzą dwie funkcje z asyncio.
1. asyncio.gather – czekamy na komplet danych
Dobre tam, gdzie sens ma tylko zestaw pełnych wyników – np. wszystkie elementy strony muszą być gotowe, zanim ją wyrenderujesz.
async def run_gather():
print("--- test gather ---")
results = await asyncio.gather(
fetch_data("obróbka obrazu", 2),
fetch_data("analiza tekstu", 1),
)
print(f"Wyniki: {results}")
gather uruchamia korutyny współbieżnie, ale zwraca listę wyników
w dokładnie takiej kolejności, w jakiej przekazałeś argumenty.
2. asyncio.as_completed – przetwarzaj na bieżąco
Gdy ważniejsze jest"kto pierwszy, ten lepszy” – np. streaming wielu źródeł,
albo chcesz od razu obsługiwać szybko odpowiadające API – bardziej pasuje
as_completed.
async def run_as_completed():
print("--- test as_completed ---")
tasks = [
fetch_data("obróbka obrazu", 2),
fetch_data("analiza tekstu", 1),
]
for coro in asyncio.as_completed(tasks):
result = await coro
print(f"Odebrano: {result}")
W tym przypadku najpierw zobaczysz wynik zadania z opóźnieniem 1 s, a dopiero później z 2 s – niezależnie od ich kolejności na liście.
IV. Częste problemy środowiskowe i jak na nie reagować
-
Brak
await
Zamiast wyniku widzisz w logach coś w stylu<coroutine object fetch_data at 0x...>– to znak, że korutyna nigdy nie została odczekana ani zamieniona na zadanie. -
RuntimeError: asyncio.run() cannot be called from a running event loop
Typowe w Jupyterze/IPythonie, gdzie event loop już działa w tle. Tam wywołujesz po prostuawait main(), zamiastasyncio.run(main()). -
Brak zysku wydajności
Kiedy wasync defużyjesztime.sleep(),requests.get()albo synchronicznego klienta DB, blokujesz jednowątkowy event loop. Zastąp toasyncio.sleep()lub bibliotekami z natywnym wsparciem async (aiohttp/httpx, async ORM itp.).
V. Kiedy warto, a kiedy nie warto sięgać po korutyny?
Korutyny najlepiej sprawdzają się tam, gdzie dużo czekasz na sieć lub dysk – zapytania HTTP, mikroserwisy, intensywne korzystanie z baz i kolejek. W takich scenariuszach asyncio potrafi znacząco zwiększyć przepustowość.
Jeżeli Twoja aplikacja głównie"mieli” dane (np. transkodowanie wideo, ciężkie ETL), dużo lepszym wyborem będzie multiprocessing albo przeniesienie krytycznych fragmentów do C/Rusta.
VI. Trzy krótkie checklisty na koniec
-
Czy Twoje I/O naprawdę jest async‑friendly?
Sprawdź, czy biblioteki używane w korutynach mają natywne wsparcie async.requestswasync defto czerwone światło. -
Czy poprawnie wywołujesz
gather()?
Jeśli masz listę korutyn, pamiętaj o*:asyncio.gather(*task_list), inaczej przekażesz listę jako jeden argument. -
Czy rozumiesz różnicę między korutyną a Taskiem?
Jeśli chcesz, żeby coś"szło w tle”, twórz Task przezasyncio.create_task(); samoawaitzawsze zatrzymuje bieżącą korutynę do czasu zakończenia wywołania.