programowanieProgramista Backend

Wyjaśnij, jak w Pythonie zrealizowano programowanie asynchroniczne za pomocą async/await. Jakie są zalety i trudności tego podejścia?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

Programowanie asynchroniczne pojawiło się w języku Python od wersji 3.5 z wprowadzeniem słów kluczowych async i await. Początkowo do zadań asynchronicznych używano takich bibliotek jak asyncio oraz generatorów opartych na korutinach, co było trudne do zrozumienia i utrzymania. Po wprowadzeniu składni async/await kod asynchroniczny stał się bardziej oczywisty i czytelny, zbliżony do tradycyjnego stylu synchronicznego.

Aspekt historyczny

Przed pojawieniem się async/await asynchroniczność była realizowana za pomocą callbacków i generatorów (na przykład z użyciem biblioteki tornado lub starych interfejsów API asyncio). Taki kod był trudny do debugowania i utrzymania.

Problem

Głównym problemem przy przetwarzaniu dużej liczby równoczesnych operacji I/O (zapytania sieciowe, operacje na plikach) w kodzie synchronicznym jest blokada głównego wątku. Prowadzi to do spadku wydajności i niemożności efektywnego wykorzystania zasobów.

Rozwiązanie

Programowanie asynchroniczne przy użyciu async/await pozwala na jednoczesne wykonywanie wielu operacji wejścia-wyjścia w ramach jednego wątku, unikając blokad. Przy tym składnia jest bliska zwykłym funkcjom, co ułatwia czytelność i debugowanie.

Przykład kodu:

import asyncio async def fetch_data(delay): print(f"Start fetching after {delay}s delay") await asyncio.sleep(delay) print(f"Done fetching after {delay}s delay") return delay async def main(): results = await asyncio.gather( fetch_data(1), fetch_data(2), fetch_data(3) ) print("Results:", results) asyncio.run(main())

Kluczowe cechy:

  • Brak blokady wątku: operacje wejścia-wyjścia zwalniają wątek.
  • Oczywista składnia (async/await) dla wywołań asynchronicznych.
  • Łatwa integracja z bibliotekami obsługującymi asyncio.

Pytania z zaskoczeniem.

Czy funkcja zdefiniowana za pomocą async def może być wywołana jak zwykła funkcja?

Nie. Wywołanie takiej funkcji zwraca obiekt korutyny, który nie jest wykonywany do momentu przekazania go do pętli zdarzeń (na przykład za pomocą await lub asyncio.run()).

def foo(): return 42 async def bar(): return 42 print(foo()) # 42 print(bar()) # <coroutine object bar at ...>

Czy można użyć await poza asynchroniczną funkcją?

Nie. Słowo kluczowe await powinno być używane tylko wewnątrz funkcji zadeklarowanych za pomocą async def. Próba użycia await poza taką funkcją spowoduje błąd składni (SyntaxError).

# Błąd! await asyncio.sleep(1) # SyntaxError: 'await' outside async function

Czy asynchroniczność działa dla operacji nie związanych z I/O (na przykład obliczenia)?

Nie. Asynchroniczność jest skuteczna tylko dla operacji wejścia-wyjścia. Dla zadań obliczeniowych wciąż potrzebny jest multiprocessing lub threading, inaczej zostanie zablokowana pętla zdarzeń.

Typowe błędy i antywzorce

Zalety:

  • Znacząco skraca czas oczekiwania na operacje I/O.
  • Zwiększa responsywność serwerów i aplikacji.
  • Utrzymuje jednowątkowy charakter kodu, omijając problemy z wielowątkowością.

Wady:

  • Kod asynchroniczny jest trudny do testowania.
  • Wymagana jest obsługa asynchroniczności przez biblioteki (nie wszystkie wspierają asyncio).
  • Błędne przekonanie, że asynchroniczność przyspiesza obliczenia — to prawda tylko dla I/O.

Przykład z życia

Negatywny przypadek: Młodzi programiści postanowili przyspieszyć aplikację za pomocą async/await, ale asynchronicznie wykonywali tylko obliczenia, a nie zapytania sieciowe. Aplikacja się nie przyspieszyła. Zalety: zapoznali się z składnią. Wady: brak zysków, kod stał się bardziej skomplikowany.

Pozytywny przypadek: Asynchronicznie obsługiwali tysiące zapytań do API. Serwer zaczął obsługiwać więcej klientów bez zwiększania zasobów. Zalety: znacznie wzrosła wydajność, architektura stała się prostsza. Wady: wzrósł próg wejścia dla nowicjuszy.