Last modified: December 06, 2023
This article is written in: 🇵🇱
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).
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.
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.
async
, które mogą być zawieszone i wznowione, co umożliwia przełączanie między zadaniami bez blokowania wątku.
--------------------------------------------------------------------------------
| 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
asyncio
: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!")
async def
zamiast def
?async def
nie powoduje jej natychmiastowego wykonania. Zamiast tego zwraca obiekt korutyny, który musi być uruchomiony przez pętlę zdarzeń.await
do zawieszania ich wykonania, oczekując na zakończenie innych korutyn lub operacji asynchronicznych.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.
Istnieje kilka sposobów na uruchomienie i zarządzanie korutynami. Omówimy trzy główne metody:
await
z innych funkcji asynchronicznychNajprostszym 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())
await asyncio.sleep(1)
.moja_korutyna
za 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.
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 wykonania moja_korutyna
w tle.main
kontynuuje wykonywanie bez oczekiwania na zakończenie moja_korutyna
.await task
powoduje, że main
zawiesza się do momentu zakończenia moja_korutyna
.Korzyści z użycia asyncio.create_task()
:
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ń.
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:
asyncio.run()
zamiast bezpośredniego manipulowania pętlą zdarzeń, chyba że istnieje konkretny powód.async
? Wykonywanie synchroniczne vs asynchroniczneAby zrozumieć różnice między kodem synchronicznym a asynchronicznym, przeanalizujmy dwa przykłady ilustrujące ich działanie.
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ń.
time.sleep(3)
, program jest zablokowany i nie może wykonywać innych operacji.
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ń.
proste_zadanie
i może wykonywać inne operacje.time.sleep(3)
blokuje cały wątek. W kodzie asynchronicznym await asyncio.sleep(3)
zawiesza tylko korutynę, pozwalając pętli zdarzeń na wykonywanie innych zadań.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]
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.
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
:
zadanie
przyjmuje dwa argumenty: numer
i czas
.await asyncio.sleep(czas)
, aby symulować czasochłonne operacje (np. żądania sieciowe).Funkcja main
:
asyncio.gather
do równoczesnego uruchomienia trzech instancji korutyny zadanie
z różnymi argumentami.asyncio.gather
zwraca listę wyników po zakończeniu wszystkich korutyn.Działanie programu:
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
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())
asyncio.create_task
, co natychmiast planuje ich wykonanie w pętli zdarzeń.main
.await taskX
, co daje możliwość kontrolowania kolejności oczekiwania na wyniki.Zalety użycia asyncio.create_task
:
cancel()
.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
asyncio.gather
.asyncio.create_task
.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.
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")
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")
Wnioski:
asyncio
jest szybsze?asyncio
zarządza kolejnością wykonywania zadań, optymalizując wykorzystanie czasu procesora.asyncio
?Przykłady zastosowań:
aiohttp
, FastAPI
czy Sanic
wykorzystują asynchroniczność do obsługi wielu żądań HTTP jednocześnie.