Programowanie asynchroniczne
Programowanie asynchroniczne to paradygmat, który umożliwia wykonywanie operacji w sposób nieblokujący, pozwalając na równoczesne przetwarzanie wielu zadań w ramach jednego wątku. W przeciwieństwie do tradycyjnego programowania synchronicznego, gdzie operacje są wykonywane sekwencyjnie i każda musi zakończyć się przed rozpoczęciem kolejnej, programowanie asynchroniczne pozwala na zawieszenie wykonania operacji, która oczekuje na wynik (np. operacji wejścia/wyjścia), i przełączenie się na wykonywanie innej operacji. Dzięki temu zasoby systemowe są wykorzystywane efektywniej, co jest szczególnie ważne w aplikacjach, które są ograniczone przez operacje I/O (tzw. I/O-bound).
Dlaczego programowanie asynchroniczne jest ważne?
- Pozwala na lepsze wykorzystanie czasu procesora poprzez przełączanie między zadaniami podczas oczekiwania na zakończenie operacji I/O.
- Umożliwia obsługę większej liczby jednoczesnych połączeń czy zadań bez konieczności tworzenia wielu wątków lub procesów.
- Pomaga uniknąć sytuacji, w których program jest zablokowany i nie może kontynuować pracy z powodu oczekiwania na zakończenie czasochłonnej operacji.
Problem z GIL w Pythonie
Python posiada mechanizm zwany Global Interpreter Lock (GIL), który ogranicza wykonywanie kodu Pythona do jednego aktywnego wątku w danym momencie, niezależnie od liczby uruchomionych wątków w programie. Oznacza to, że nawet jeśli stworzymy wiele wątków, tylko jeden z nich może jednocześnie wykonywać instrukcje Pythona, co może znacząco ograniczać wydajność aplikacji wielowątkowych, szczególnie tych intensywnie korzystających z procesora. Aby obejść to ograniczenie, programiści mogą skorzystać z biblioteki asyncio, która umożliwia asynchroniczne wykonywanie operacji w ramach jednego wątku. Dzięki asyncio możliwe jest efektywne zarządzanie wieloma operacjami wejścia/wyjścia bez konieczności tworzenia i synchronizacji wielu wątków, co upraszcza kod, redukuje narzut związany z zarządzaniem wątkami oraz zwiększa wydajność aplikacji w kontekście operacji I/O. W praktyce, wykorzystując asyncio, można realizować wiele zadań jednocześnie w jednym wątku, co eliminuje problemy związane z GIL i pozwala na tworzenie bardziej skalowalnych oraz responsywnych programów.
Wprowadzenie do asyncio
asyncio to biblioteka standardowa w Pythonie (od wersji 3.4), która wprowadza wsparcie dla programowania asynchronicznego za pomocą korutyn, pętli zdarzeń, zadań i przyszłości (ang. futures). Pozwala ona na pisanie jednowątkowego kodu asynchronicznego, który jest zarówno czytelny, jak i wydajny.
- Korutyny to specjalne funkcje oznaczone słowem kluczowym
async, które mogą być zawieszone i wznowione, co umożliwia przełączanie między zadaniami bez blokowania wątku. - Pętla zdarzeń to mechanizm zarządzający wykonywaniem korutyn i obsługą zdarzeń asynchronicznych.
- Zadania (Tasks) są abstrakcją reprezentującą wykonywanie korutyn, która może być zarządzana przez pętlę zdarzeń.
--------------------------------------------------------------------------------
| Synchroniczne (Blokujące) Wywołanie Funkcji |
--------------------------------------------------------------------------------
Oś czasu (od góry do dołu)
Wywołujący (np. program główny) Funkcja Foo (synch)
| (1) Wywołujący rozpoczyna
|
| (2) Wywołujący wywołuje Foo() +--------------------------------+
|-------------------------------> | Foo() zaczyna wykonywać swoje |
| | zadania |
| | - Możliwe operacje We/Wy |
| | - Lub obliczenia |
| +--------------+-----------------+
| |
| (3) Wywołujący jest zablokowany |
| aż Foo() zakończy swoje zadania |
| |
| +--------------v--------------------+
| | Foo() kończy zadania, zwraca dane |
|<--------------------------------+ np. wynik lub kod statusu |
| +--------------+--------------------+
|
| (4) Wywołujący otrzymuje wynik
| i kontynuuje
|
V
--------------------------------------------------------------------------------
| Asynchroniczne (Niezablokujące) Wywołanie Funkcji |
--------------------------------------------------------------------------------
Oś czasu (od góry do dołu)
Wywołujący (np. program główny) Funkcja Bar (async)
| (1) Wywołujący rozpoczyna
|
| (2) Wywołujący wywołuje BarAsync()
|------------------------------------------> +-----------------------------------+
| | BarAsync() inicjuje swoje zadania |
| (3) BarAsync() +-----------------------------------+
| natychmiast zwraca kontrolę
|
| (4) Wywołujący NIE jest zablokowany;
| może kontynuować inne zadania
|
| <---- Możliwe wykonywanie dalszej logiki, obsługa interfejsu użytkownika, itp. --+
|
| (5) W międzyczasie, BarAsync() kończy swoje zadanie w tle
| (odpowiedź sieciowa, odczyt pliku, itp.)
|
| +-----------------------------------------------+
|<------------------------> | BarAsync() wywołuje zwrotnie Wywołującego |
| | (np. funkcja zwrotna lub rozwiązanie promise) |
| +-----------------------------------------------+
|
| (6) Wywołujący obsługuje otrzymany wynik
| który właśnie nadeszedł
|
V
Zalety asyncio:
- Pozwala na obsługę tysięcy jednoczesnych połączeń bez znaczącego obciążenia systemu.
- Umożliwia pisanie asynchronicznego kodu w sposób zbliżony do kodu synchronicznego.
- Łatwo integruje się z innymi bibliotekami i frameworkami asynchronicznymi.
Podstawy asyncio
Aby funkcja mogła stać się korutyną, należy zadeklarować ją przy użyciu słowa kluczowego async def. Taka funkcja zwraca obiekt korutyny, który reprezentuje jej przyszłe wykonanie.
Przykład:
async def moja_korutyna():
print("Witaj!")
Co zmienia się, gdy używamy async def zamiast def?
- Wywołanie funkcji zdefiniowanej jako
async defnie powoduje jej natychmiastowego wykonania. Zamiast tego zwraca obiekt korutyny, który musi być uruchomiony przez pętlę zdarzeń. - Wewnątrz korutyn możemy używać
awaitdo zawieszania ich wykonania, oczekując na zakończenie innych korutyn lub operacji asynchronicznych. - Korutyny mogą być zawieszane w trakcie działania i wznawiane później, co pozwala na efektywne przełączanie między różnymi zadaniami.
Rola pętli zdarzeń
Pętla zdarzeń jest centralnym mechanizmem w asyncio, odpowiedzialnym za planowanie i wykonywanie korutyn. Monitoruje ona stan wszystkich zadań i decyduje, które z nich mogą być wykonane w danym momencie. Dzięki temu możliwe jest równoczesne zarządzanie wieloma operacjami asynchronicznymi w jednym wątku.
Wywoływanie korutyn
Istnieje kilka sposobów na uruchomienie i zarządzanie korutynami. Omówimy trzy główne metody:
I. Wywoływanie za pomocą await z innych funkcji asynchronicznych
Najprostszym sposobem uruchomienia korutyny jest użycie await wewnątrz innej korutyny. Pozwala to na sekwencyjne wykonywanie operacji asynchronicznych.
Przykład:
import asyncio
async def moja_korutyna():
print("Początek korutyny")
await asyncio.sleep(1) # Zawieszenie korutyny na 1 sekundę
print("Koniec korutyny po 1 sekundzie")
async def main():
print("Rozpoczynam")
await moja_korutyna() # Czekamy na zakończenie korutyny
print("Zakończono")
asyncio.run(main())
- Funkcja asynchroniczna, która zawiesza swoje wykonanie na 1 sekundę przy użyciu
await asyncio.sleep(1). - Główna funkcja asynchroniczna, która wywołuje
moja_korutynaza pomocąawait. asyncio.run(main())inicjuje pętlę zdarzeń i uruchamia korutynęmain.
Dlaczego używamy await?
Słowo kluczowe await powoduje, że korutyna zostaje zawieszona do momentu zakończenia oczekiwanej operacji. W tym czasie pętla zdarzeń może przydzielić zasoby innym korutynom, co zwiększa efektywność programu.
Porównanie z kodem synchronicznym:
import time
def moja_funkcja():
print("Początek funkcji")
time.sleep(1) # Zawieszenie funkcji na 1 sekundę
print("Koniec funkcji po 1 sekundzie")
def main():
print("Rozpoczynam")
moja_funkcja() # Blokujemy wykonanie do zakończenia funkcji
print("Zakończono")
if __name__ == "__main__":
main()
W wersji synchronicznej, podczas wykonywania time.sleep(1), program jest zablokowany i nie może wykonywać innych operacji. W wersji asynchronicznej, pętla zdarzeń może przełączać się między różnymi korutynami.
II. Uruchamianie korutyn równolegle za pomocą asyncio.create_task()
Aby wykonywać korutyny równocześnie (w sensie asynchronicznym), możemy użyć asyncio.create_task(), który tworzy zadanie zarządzane przez pętlę zdarzeń.
Przykład:
import asyncio
async def moja_korutyna():
print("Początek korutyny")
await asyncio.sleep(1)
print("Koniec korutyny po 1 sekundzie")
async def main():
print("Rozpoczynam")
task = asyncio.create_task(moja_korutyna()) # Tworzymy zadanie asynchroniczne
print("Inne działania w main()")
await task # Czekamy na zakończenie zadania
print("Zakończono")
asyncio.run(main())
asyncio.create_task(moja_korutyna())informuje pętlę zdarzeń o konieczności wykonaniamoja_korutynaw tle.- Po utworzeniu zadania, korutyna
mainkontynuuje wykonywanie bez oczekiwania na zakończeniemoja_korutyna. await taskpowoduje, żemainzawiesza się do momentu zakończeniamoja_korutyna.
Korzyści z użycia asyncio.create_task():
- Pozwala na uruchomienie wielu korutyn jednocześnie, co zwiększa efektywność programu.
- Możemy zarządzać zadaniami, np. anulować je, jeśli nie są już potrzebne.
Przykład z wieloma zadaniami:
import asyncio
async def zadanie(nr, czas):
print(f"Zadanie {nr} rozpoczęte")
await asyncio.sleep(czas)
print(f"Zadanie {nr} zakończone po {czas} sekundach")
async def main():
task1 = asyncio.create_task(zadanie(1, 2))
task2 = asyncio.create_task(zadanie(2, 3))
task3 = asyncio.create_task(zadanie(3, 1))
await task1
await task2
await task3
asyncio.run(main())
Wynik:
Zadanie 1 rozpoczęte
Zadanie 2 rozpoczęte
Zadanie 3 rozpoczęte
Zadanie 3 zakończone po 1 sekundach
Zadanie 1 zakończone po 2 sekundach
Zadanie 2 zakończone po 3 sekundach
W tym przykładzie wszystkie zadania są uruchamiane niemal jednocześnie, a ich zakończenie zależy od czasu trwania poszczególnych zadań.
III. Uruchamianie korutyn ze zwykłych funkcji za pomocą pętli zdarzeń
Jeśli chcemy uruchomić korutynę z funkcji synchronicznej (zwykłej funkcji), możemy bezpośrednio użyć pętli zdarzeń.
Przykład:
import asyncio
async def moja_korutyna():
print("Początek korutyny")
await asyncio.sleep(1)
print("Koniec korutyny po 1 sekundzie")
def main():
loop = asyncio.get_event_loop() # Pobieramy bieżącą pętlę zdarzeń
loop.run_until_complete(moja_korutyna()) # Uruchamiamy korutynę i czekamy na jej zakończenie
main()
asyncio.get_event_loop()zwraca bieżącą pętlę zdarzeń lub tworzy nową, jeśli żadna nie istnieje.loop.run_until_complete(moja_korutyna())uruchamia korutynę i blokuje wykonanie do jej zakończenia.
Uwaga:
- Od Pythona 3.7 zaleca się używanie
asyncio.run()zamiast bezpośredniego manipulowania pętlą zdarzeń, chyba że istnieje konkretny powód. - Bezpośrednie użycie pętli zdarzeń jest przydatne w bardziej złożonych scenariuszach, np. w aplikacjach GUI czy serwerach, gdzie pętla zdarzeń jest zarządzana ręcznie.
Co zmienia async? Wykonywanie synchroniczne vs asynchroniczne
Aby zrozumieć różnice między kodem synchronicznym a asynchronicznym, przeanalizujmy dwa przykłady ilustrujące ich działanie.
Kod synchroniczny
import time
def proste_zadanie():
print("Pracownik przetwarza zadania...")
time.sleep(3) # Symulacja czasochłonnego zadania
print("Pracownik skończył zadanie.")
return 42
def main():
print("Rozpoczynamy główne zadanie.")
wynik = proste_zadanie() # Blokujemy wykonanie do zakończenia zadania
print("Menadżer musiał czekać!")
print(f"Pracownik odpowiedział, że ukończył {wynik} zadań.")
if __name__ == "__main__":
main()
Wynik:
Rozpoczynamy główne zadanie.
Pracownik przetwarza zadania...
Pracownik skończył zadanie.
Menadżer musiał czekać!
Pracownik odpowiedział, że ukończył 42 zadań.
- Podczas wykonywania
time.sleep(3), program jest zablokowany i nie może wykonywać innych operacji. - Menadżer musi czekać, aż pracownik skończy zadanie, zanim może kontynuować.
Kod asynchroniczny
import asyncio
async def proste_zadanie():
print("Pracownik przetwarza zadania...")
await asyncio.sleep(3) # Asynchroniczna symulacja czasochłonnego zadania
print("Pracownik skończył zadanie.")
return 42
async def main():
print("Menadżer pyta pracownika o postęp.")
task = asyncio.create_task(proste_zadanie()) # Uruchamiamy zadanie asynchronicznie
print("Menadżer może wykonywać inne zadania w międzyczasie...")
wynik = await task # Oczekujemy na zakończenie zadania
print(f"Pracownik odpowiedział, że ukończył {wynik} zadań.")
asyncio.run(main())
Wynik:
Menadżer pyta pracownika o postęp.
Menadżer może wykonywać inne zadania w międzyczasie...
Pracownik przetwarza zadania...
Pracownik skończył zadanie.
Pracownik odpowiedział, że ukończył 42 zadań.
- Menadżer nie jest zablokowany podczas wykonywania
proste_zadaniei może wykonywać inne operacje. - Asynchroniczność pozwala na efektywne wykorzystanie czasu oczekiwania.
Różnice między kodem synchronicznym a asynchronicznym
- W kodzie synchronicznym
time.sleep(3)blokuje cały wątek. W kodzie asynchronicznymawait asyncio.sleep(3)zawiesza tylko korutynę, pozwalając pętli zdarzeń na wykonywanie innych zadań. - Asynchroniczność pozwala na lepsze wykorzystanie czasu procesora przez przełączanie między zadaniami podczas oczekiwania na operacje I/O.
- Kod asynchroniczny jest bardziej skalowalny w kontekście obsługi wielu jednoczesnych zadań.
Przykład z wieloma pracownikami
Rozszerzmy przykład, aby pokazać, jak asynchroniczność pozwala na równoczesne wykonywanie wielu zadań.
import asyncio
async def pracownik(nr, czas):
print(f"Pracownik {nr} rozpoczął zadanie.")
await asyncio.sleep(czas)
print(f"Pracownik {nr} skończył zadanie po {czas} sekundach.")
return nr * 10
async def main():
print("Menadżer zleca zadania pracownikom.")
tasks = [
asyncio.create_task(pracownik(1, 2)),
asyncio.create_task(pracownik(2, 3)),
asyncio.create_task(pracownik(3, 1)),
]
print("Menadżer może wykonywać inne zadania w międzyczasie...")
wyniki = await asyncio.gather(*tasks)
print(f"Pracownicy ukończyli zadania z wynikami: {wyniki}")
asyncio.run(main())
Wynik:
Menadżer zleca zadania pracownikom.
Menadżer może wykonywać inne zadania w międzyczasie...
Pracownik 1 rozpoczął zadanie.
Pracownik 2 rozpoczął zadanie.
Pracownik 3 rozpoczął zadanie.
Pracownik 3 skończył zadanie po 1 sekundach.
Pracownik 1 skończył zadanie po 2 sekundach.
Pracownik 2 skończył zadanie po 3 sekundach.
Pracownicy ukończyli zadania z wynikami: [10, 20, 30]
Wykonywanie wielu korutyn równocześnie
Asynchroniczność w Pythonie, za pomocą biblioteki asyncio, pozwala na równoczesne wykonywanie wielu korutyn. Jest to szczególnie przydatne, gdy mamy wiele niezależnych zadań, które mogą być wykonywane jednocześnie, takich jak żądania sieciowe, operacje na plikach czy interakcje z bazami danych. Dzięki temu możemy znacząco zwiększyć efektywność i wydajność naszej aplikacji.
Uruchamianie wielu korutyn za pomocą asyncio.gather
Funkcja asyncio.gather umożliwia jednoczesne uruchomienie wielu korutyn i oczekiwanie na ich zakończenie. Przyjrzyjmy się temu na konkretnym przykładzie.
Przykład:
import asyncio
async def zadanie(numer, czas):
print(f"Zadanie {numer} rozpoczęte...")
await asyncio.sleep(czas)
print(f"Zadanie {numer} zakończone po {czas} sekundach.")
return f"Wynik zadania {numer}"
async def main():
print("Rozpoczynamy wykonywanie wielu zadań równocześnie.")
wyniki = await asyncio.gather(
zadanie(1, 2),
zadanie(2, 3),
zadanie(3, 1)
)
print("Wszystkie zadania zostały zakończone.")
for wynik in wyniki:
print(wynik)
if __name__ == "__main__":
asyncio.run(main())
Korutyna zadanie:
- Funkcja
zadanieprzyjmuje dwa argumenty:numericzas. - Wyświetla komunikat o rozpoczęciu zadania.
- Używa
await asyncio.sleep(czas), aby symulować czasochłonne operacje (np. żądania sieciowe). - Po upływie określonego czasu wyświetla komunikat o zakończeniu zadania.
- Zwraca wynik w postaci napisu.
Funkcja main:
- Wyświetla komunikat o rozpoczęciu wykonywania zadań.
- Używa
asyncio.gatherdo równoczesnego uruchomienia trzech instancji korutynyzadaniez różnymi argumentami. asyncio.gatherzwraca listę wyników po zakończeniu wszystkich korutyn.- Po otrzymaniu wyników, wyświetla komunikaty o zakończeniu i wypisuje wyniki poszczególnych zadań.
Działanie programu:
- Wszystkie trzy zadania są uruchamiane niemal jednocześnie.
- Korutyny są wykonywane równolegle (w ramach jednego wątku), co oznacza, że czas wykonania całego zestawu zadań jest zbliżony do czasu najdłuższego pojedynczego zadania (w tym przypadku 3 sekundy).
- W trakcie oczekiwania na zakończenie jednego zadania, inne zadania mogą być wykonywane.
Wynik działania programu:
Rozpoczynamy wykonywanie wielu zadań równocześnie.
Zadanie 1 rozpoczęte...
Zadanie 2 rozpoczęte...
Zadanie 3 rozpoczęte...
Zadanie 3 zakończone po 1 sekundach.
Zadanie 1 zakończone po 2 sekundach.
Zadanie 2 zakończone po 3 sekundach.
Wszystkie zadania zostały zakończone.
Wynik zadania 1
Wynik zadania 2
Wynik zadania 3
Uruchamianie wielu korutyn za pomocą asyncio.create_task
Alternatywnym sposobem jest użycie funkcji asyncio.create_task, która tworzy zadania asynchroniczne z korutyn. Pozwala to na większą kontrolę nad poszczególnymi zadaniami, np. możliwość ich anulowania czy monitorowania stanu.
Przykład:
import asyncio
async def zadanie(numer, czas):
print(f"Zadanie {numer} rozpoczęte...")
await asyncio.sleep(czas)
print(f"Zadanie {numer} zakończone po {czas} sekundach.")
return f"Wynik zadania {numer}"
async def main():
print("Rozpoczynamy wykonywanie wielu zadań równocześnie.")
task1 = asyncio.create_task(zadanie(1, 2))
task2 = asyncio.create_task(zadanie(2, 3))
task3 = asyncio.create_task(zadanie(3, 1))
# W tym miejscu możemy wykonywać inne operacje
print("Wykonuję inne operacje w main()...")
# Oczekiwanie na zakończenie zadań
wynik1 = await task1
wynik2 = await task2
wynik3 = await task3
print("Wszystkie zadania zostały zakończone.")
print(wynik1)
print(wynik2)
print(wynik3)
if __name__ == "__main__":
asyncio.run(main())
- Tworzymy zadania za pomocą
asyncio.create_task, co natychmiast planuje ich wykonanie w pętli zdarzeń. - Możemy w międzyczasie wykonywać inne operacje w funkcji
main. - Każde zadanie jest oczekiwane indywidualnie za pomocą
await taskX, co daje możliwość kontrolowania kolejności oczekiwania na wyniki.
Zalety użycia asyncio.create_task:
- Możemy kontrolować każde zadanie z osobna.
- Zadania można anulować za pomocą metody
cancel(). - Możemy sprawdzać stan zadania (np. czy jest w trakcie wykonywania, zakończone czy anulowane).
Różnice między asyncio.gather a asyncio.create_task
| Cechy | asyncio.gather |
asyncio.create_task |
|---|---|---|
| Sposób Uruchamiania Korutyn | Uruchamia wszystkie korutyny równocześnie i czeka na ich zakończenie. | Tworzy zadanie (Task) do wykonania w pętli zdarzeń. |
| Kolejność Wyników | Zwraca listę wyników w tej samej kolejności, w jakiej korutyny zostały przekazane. | Brak gwarantowanej kolejności wyników, zadania są wykonywane niezależnie. |
| Obsługa Wyjątków | Przerywa działanie i propaguje wyjątek, chyba że użyto return_exceptions=True, wtedy zwraca wyjątki jako wyniki. |
Wyjątki w korutynach muszą być obsługiwane ręcznie, nie przerywa to działania innych zadań. |
| Kontrola Zadania | Brak bezpośredniej kontroli nad zadaniami, oczekuje na zakończenie wszystkich korutyn. | Pozwala na anulowanie, sprawdzanie stanu zadania i dodawanie callbacków. |
Praktyczne zastosowania
- Gdy potrzebujemy prostego sposobu na równoczesne uruchomienie wielu korutyn i zebranie wyników używamy
asyncio.gather. - Gdy potrzebujemy większej kontroli nad zadaniami używamy
asyncio.create_task. - Np. gdy chcemy anulować zadanie po określonym czasie lub gdy chcemy reagować na jego zakończenie za pomocą callbacków.
Jak asyncio zwiększa wydajność
Wykorzystanie asynchroniczności pozwala na efektywniejsze zarządzanie czasem procesora i operacjami I/O. W tradycyjnym podejściu synchronicznym, gdy program napotka operację I/O, taką jak żądanie sieciowe czy odczyt pliku, musi czekać na jej zakończenie, zanim przejdzie do kolejnej instrukcji. Oznacza to, że czas procesora jest marnowany na bezczynne oczekiwanie.
W asynchroniczności, podczas gdy jedno zadanie czeka na operację I/O, pętla zdarzeń asyncio może przełączać się na wykonywanie innych korutyn, które są gotowe do działania. Dzięki temu maksymalizujemy wykorzystanie dostępnego czasu procesora i skracamy ogólny czas wykonywania programu.
Przykład: Porównanie wydajności żądań HTTP
Załóżmy, że chcemy wysłać 10 żądań HTTP do tego samego adresu URL.
Podejście synchroniczne:
import requests
import time
adresy_url = ["https://jsonplaceholder.typicode.com/posts/1" for _ in range(10)]
start = time.time()
for adres in adresy_url:
odpowiedz = requests.get(adres)
print(f"Status: {odpowiedz.status_code}")
end = time.time()
print(f"Synchronicznie: {end - start:.2f} sekund")
- Każde żądanie jest wysyłane po zakończeniu poprzedniego.
- Całkowity czas wykonania to suma czasów poszczególnych żądań.
- Jeśli każde żądanie trwa około 0.5 sekundy, to 10 żądań zajmie około 5 sekund.
Podejście asynchroniczne:
import aiohttp
import asyncio
import time
adresy_url = ["https://jsonplaceholder.typicode.com/posts/1" for _ in range(10)]
async def pobierz(adres, sesja):
async with sesja.get(adres) as odpowiedz:
print(f"Status: {odpowiedz.status}")
return await odpowiedz.text()
async def main():
async with aiohttp.ClientSession() as sesja:
zadania = [asyncio.create_task(pobierz(adres, sesja)) for adres in adresy_url]
await asyncio.gather(*zadania)
start = time.time()
asyncio.run(main())
end = time.time()
print(f"Asynchronicznie: {end - start:.2f} sekund")
- Wszystkie żądania są wysyłane niemal jednocześnie.
- Całkowity czas wykonania zbliża się do czasu najdłuższego pojedynczego żądania.
- Jeśli każde żądanie trwa około 0.5 sekundy, to 10 żądań zajmie około 0.5-1 sekundy.
Wnioski:
- Asynchroniczność znacząco skraca czas wykonania operacji I/O, gdyż pozwala na równoczesne oczekiwanie na wyniki wielu żądań.
- Program nie marnuje czasu na bezczynne oczekiwanie, lecz wykorzystuje go na wykonywanie innych zadań.
- Asynchroniczne programy mogą obsługiwać więcej zadań bez proporcjonalnego zwiększania zapotrzebowania na zasoby.
Dlaczego asyncio jest szybsze?
- Podczas gdy jedno zadanie czeka na operację I/O, inne mogą być wykonywane.
- Asynchroniczność nie wymaga tworzenia nowych wątków czy procesów, co zmniejsza narzut związany z przełączaniem kontekstu i zużyciem pamięci.
asynciozarządza kolejnością wykonywania zadań, optymalizując wykorzystanie czasu procesora.
Kiedy używać asyncio?
- Gdy aplikacja wysyła wiele żądań sieciowych lub obsługuje wiele połączeń (np. serwery HTTP, klienty API).
- Gdy potrzebujemy równocześnie czytać lub zapisywać wiele plików.
- Przy równoczesnym wykonywaniu wielu zapytań do bazy danych.
- Gdy wymagana jest szybka reakcja na zdarzenia (np. aplikacje IoT, komunikatory).
- Gdy głównym ograniczeniem jest czas oczekiwania na operacje I/O, a nie moc obliczeniowa CPU.
Przykłady zastosowań:
- Frameworki takie jak
aiohttp,FastAPIczySanicwykorzystują asynchroniczność do obsługi wielu żądań HTTP jednocześnie. - Pisanie klienta, który komunikuje się z wieloma serwisami API w sposób efektywny.
- Obsługa połączeń WebSocket, gdzie utrzymujemy otwarte połączenia z wieloma klientami.
- Równoczesne pobieranie treści z wielu stron internetowych.