High-Performance Python: Asyncio
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Concurrency-Programmierung ist ein Programmieransatz, der sich mit der gleichzeitigen Ausführung mehrerer Aufgaben befasst. In Python ist asyncio
ein leistungsstarkes Werkzeug zur Implementierung asynchroner Programmierung. Basierend auf dem Konzept der Coroutinen kann asyncio
I/O-intensive Aufgaben effizient bearbeiten. Dieser Artikel stellt die grundlegenden Prinzipien und die Verwendung von asyncio
vor.
Warum wir asyncio brauchen
Wir wissen, dass die Verwendung von Multithreading beim Umgang mit I/O-Operationen die Effizienz im Vergleich zu einem normalen einzelnen Thread erheblich verbessern kann. Warum brauchen wir also noch asyncio
?
Multithreading hat viele Vorteile und ist weit verbreitet, aber es hat auch bestimmte Einschränkungen:
- Zum Beispiel kann der laufende Prozess von Multithreading leicht unterbrochen werden, sodass die Situation einer Race Condition auftreten kann.
- Darüber hinaus entstehen bei der Thread-Umschaltung selbst gewisse Kosten, und die Anzahl der Threads kann nicht unbegrenzt erhöht werden. Wenn Ihre I/O-Operationen sehr umfangreich sind, kann Multithreading die Anforderungen an hohe Effizienz und hohe Qualität wahrscheinlich nicht erfüllen.
Gerade um diese Probleme zu lösen, ist asyncio
entstanden.
Sync VS Async
Lassen Sie uns zunächst zwischen den Konzepten Sync (synchron) und Async (asynchron) unterscheiden.
- Sync bedeutet, dass Operationen nacheinander ausgeführt werden. Die nächste Operation kann erst ausgeführt werden, nachdem die vorherige abgeschlossen ist.
- Async bedeutet, dass verschiedene Operationen abwechselnd ausgeführt werden können. Wenn eine der Operationen blockiert ist, wartet das Programm nicht, sondern sucht nach ausführbaren Operationen, um fortzufahren.
Wie asyncio funktioniert
- Coroutinen:
asyncio
verwendet Coroutinen, um asynchrone Operationen zu erreichen. Eine Coroutine ist eine spezielle Funktion, die mit dem Schlüsselwortasync
definiert wird. In einer Coroutine kann das Schlüsselwortawait
verwendet werden, um die Ausführung der aktuellen Coroutine zu pausieren und auf den Abschluss einer asynchronen Operation zu warten. - Ereignisschleife: Die Ereignisschleife ist einer der Kernmechanismen von
asyncio
. Sie ist für die Planung und Ausführung von Coroutinen und die Abwicklung des Umschaltens zwischen Coroutinen verantwortlich. Die Ereignisschleife fragt ständig nach ausführbaren Aufgaben. Sobald eine Aufgabe bereit ist (z. B. wenn eine I/O-Operation abgeschlossen ist oder ein Timer abläuft), fügt die Ereignisschleife sie in die Ausführungswarteschlange ein und fährt mit der nächsten Aufgabe fort. - Asynchrone Aufgaben: In
asyncio
führen wir Coroutinen aus, indem wir asynchrone Aufgaben erstellen. Asynchrone Aufgaben werden durch die Funktionasyncio.create_task()
erstellt, die die Coroutine in ein awaitable-Objekt einkapselt und zur Verarbeitung an die Ereignisschleife übergibt. - Asynchrone I/O-Operationen:
asyncio
bietet eine Reihe von asynchronen I/O-Operationen (wie Netzwerkanfragen, Dateilesen und -schreiben usw.), die durch das Schlüsselwortawait
nahtlos in Coroutinen und die Ereignisschleife integriert werden können. Durch die Verwendung asynchroner I/O-Operationen kann eine Blockierung während des Wartens auf den Abschluss von I/O-Operationen vermieden werden, was die Programmleistung und die Parallelität verbessert. - Callbacks:
asyncio
unterstützt auch die Verwendung von Callback-Funktionen zur Behandlung der Ergebnisse asynchroner Operationen. Die Funktionasyncio.ensure_future()
kann verwendet werden, um die Callback-Funktion in ein awaitable-Objekt zu kapseln und zur Verarbeitung an die Ereignisschleife zu übergeben. - Gleichzeitige Ausführung:
asyncio
kann mehrere Coroutine-Aufgaben gleichzeitig ausführen. Die Ereignisschleife plant die Ausführung von Coroutinen automatisch entsprechend der Bereitschaft der Aufgaben und erreicht so eine effiziente parallele Programmierung.
Zusammenfassend lässt sich sagen, dass das Funktionsprinzip von asyncio
auf den Mechanismen von Coroutinen und Ereignisschleifen basiert. Durch die Verwendung von Coroutinen für asynchrone Operationen und die Zuständigkeit der Ereignisschleife für die Planung und Ausführung von Coroutinen realisiert asyncio
ein effizientes asynchrones Programmiermodell.
Coroutinen und asynchrone Programmierung
Coroutinen sind ein wichtiges Konzept in asyncio
. Sie sind schlanke Ausführungseinheiten, die schnell zwischen Aufgaben wechseln können, ohne den Overhead der Thread-Umschaltung. Coroutinen können mit dem Schlüsselwort async
definiert werden, und das Schlüsselwort await
wird verwendet, um die Ausführung der Coroutine zu pausieren und nach Abschluss einer bestimmten Operation fortzusetzen.
Hier ist ein einfaches Codebeispiel, das die Verwendung von Coroutinen für die asynchrone Programmierung demonstriert:
import asyncio async def hello(): print("Hallo") await asyncio.sleep(1) # Simulieren Sie eine zeitaufwändige Operation print("Welt") # Erstellen Sie eine Ereignisschleife loop = asyncio.get_event_loop() # Fügen Sie die Coroutine der Ereignisschleife hinzu und führen Sie sie aus loop.run_until_complete(hello())
In diesem Beispiel ist die Funktion hello()
eine Coroutine, die mit dem Schlüsselwort async
definiert wird. Innerhalb der Coroutine können wir await
verwenden, um ihre Ausführung zu pausieren. Hier wird asyncio.sleep(1)
verwendet, um eine zeitaufwändige Operation zu simulieren. Die Methode run_until_complete()
fügt die Coroutine der Ereignisschleife hinzu und führt sie aus.
Asynchrone I/O-Operationen
asyncio
wird hauptsächlich zur Bearbeitung von I/O-intensiven Aufgaben verwendet, wie z. B. Netzwerkanfragen, Dateilesen und -schreiben. Es bietet eine Reihe von APIs für asynchrone I/O-Operationen, die in Kombination mit dem Schlüsselwort await
verwendet werden können, um auf einfache Weise eine asynchrone Programmierung zu erreichen.
Hier ist ein einfaches Codebeispiel, das zeigt, wie asyncio
für asynchrone Netzwerkanfragen verwendet wird:
import asyncio import aiohttp async def fetch(session, url): async with session.get(url) as response: return await response.text() async def main(): async with aiohttp.ClientSession() as session: html = await fetch(session, 'https://www.example.com') print(html) # Erstellen Sie eine Ereignisschleife loop = asyncio.get_event_loop() # Fügen Sie die Coroutine der Ereignisschleife hinzu und führen Sie sie aus loop.run_until_complete(main())
In diesem Beispiel verwenden wir die Bibliothek aiohttp
für Netzwerkanfragen. Die Funktion fetch()
ist eine Coroutine. Sie initiiert eine asynchrone GET-Anfrage über die Methode session.get()
und wartet mit dem Schlüsselwort await
auf die Rückgabe der Antwort. Die Funktion main()
ist eine weitere Coroutine. Sie erstellt darin ein ClientSession
-Objekt zur Wiederverwendung, ruft dann die Methode fetch()
auf, um den Inhalt der Webseite abzurufen und auszugeben.
Hinweis: Hier verwenden wir aiohttp
anstelle der Bibliothek requests
, da die Bibliothek requests
nicht mit asyncio
kompatibel ist, während die Bibliothek aiohttp
dies ist. Um asyncio
gut zu nutzen, insbesondere um seine leistungsstarken Funktionen zu nutzen, sind in vielen Fällen entsprechende Python-Bibliotheken erforderlich.
Gleichzeitige Ausführung mehrerer Aufgaben
asyncio
bietet auch einige Mechanismen zur gleichzeitigen Ausführung mehrerer Aufgaben, wie z. B. asyncio.gather()
und asyncio.wait()
. Das Folgende ist ein Codebeispiel, das zeigt, wie diese Mechanismen verwendet werden, um mehrere Coroutine-Aufgaben gleichzeitig auszuführen:
import asyncio async def task1(): print("Aufgabe 1 gestartet") await asyncio.sleep(1) print("Aufgabe 1 beendet") async def task2(): print("Aufgabe 2 gestartet") await asyncio.sleep(2) print("Aufgabe 2 beendet") async def main(): await asyncio.gather(task1(), task2()) # Erstellen Sie eine Ereignisschleife loop = asyncio.get_event_loop() # Fügen Sie die Coroutine der Ereignisschleife hinzu und führen Sie sie aus loop.run_until_complete(main())
In diesem Beispiel definieren wir zwei Coroutine-Aufgaben task1()
und task2()
, die beide einige zeitaufwändige Operationen ausführen. Die Coroutine main()
startet diese beiden Aufgaben gleichzeitig über asyncio.gather()
und wartet auf deren Abschluss. Die gleichzeitige Ausführung kann die Ausführungseffizienz des Programms verbessern.
Wie wählt man aus?
Sollten wir in tatsächlichen Projekten Multithreading oder asyncio
wählen? Ein grosser Wurf hat es anschaulich zusammengefasst:
if io_bound: if io_slow: print('Use Asyncio') else: print('Use multi-threading') elif cpu_bound: print('Use multi-processing')
- Wenn es sich um I/O-gebunden handelt und die I/O-Operationen langsam sind, was die Zusammenarbeit vieler Aufgaben/Threads erfordert, dann ist die Verwendung von
asyncio
besser geeignet. - Wenn es sich um I/O-gebunden handelt, die I/O-Operationen jedoch schnell sind und nur eine begrenzte Anzahl von Aufgaben/Threads benötigt wird, dann reicht Multithreading aus.
- Wenn es sich um CPU-gebunden handelt, ist eine Multi-Processing erforderlich, um die Ausführungseffizienz des Programms zu verbessern.
Üben
Geben Sie eine Liste ein. Für jedes Element in der Liste möchten wir die Summe der Quadrate aller ganzen Zahlen von 0 bis zu diesem Element berechnen.
Synchrone Implementierung
import time def cpu_bound(number): return sum(i * i for i in range(number)) def calculate_sums(numbers): for number in numbers: cpu_bound(number) def main(): start_time = time.perf_counter() numbers = [10000000 + x for x in range(20)] calculate_sums(numbers) end_time = time.perf_counter() print('Die Berechnung dauert {} Sekunden'.format(end_time - start_time)) if __name__ == '__main__': main()
Die Ausführungszeit beträgt Die Berechnung dauert 16.00943413000002 Sekunden
Asynchrone Implementierung mit concurrent.futures
import time from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed def cpu_bound(number): return sum(i * i for i in range(number)) def calculate_sums(numbers): with ProcessPoolExecutor() as executor: results = executor.map(cpu_bound, numbers) results = [result for result in results] print(results) def main(): start_time = time.perf_counter() numbers = [10000000 + x for x in range(20)] calculate_sums(numbers) end_time = time.perf_counter() print('Die Berechnung dauert {} Sekunden'.format(end_time - start_time)) if __name__ == '__main__': main()
Die Ausführungszeit beträgt Die Berechnung dauert 7.314132894999999 Sekunden
In diesem verbesserten Code verwenden wir concurrent.futures.ProcessPoolExecutor
, um einen Prozesspool zu erstellen, und verwenden dann die Methode executor.map()
, um Aufgaben zu übermitteln und Ergebnisse zu erhalten. Beachten Sie, dass Sie nach der Verwendung von executor.map()
die Ergebnisse in eine Liste iterieren oder andere Methoden verwenden können, um die Ergebnisse zu verarbeiten, wenn Sie die Ergebnisse abrufen müssen.
Multiprocessing-Implementierung
import time import multiprocessing def cpu_bound(number): return sum(i * i for i in range(number)) def calculate_sums(numbers): with multiprocessing.Pool() as pool: pool.map(cpu_bound, numbers) def main(): start_time = time.perf_counter() numbers = [10000000 + x for x in range(20)] calculate_sums(numbers) end_time = time.perf_counter() print('Die Berechnung dauert {} Sekunden'.format(end_time - start_time)) if __name__ == '__main__': main()
Die Ausführungszeit beträgt Die Berechnung dauert 5.024221667 Sekunden
concurrent.futures.ProcessPoolExecutor
und multiprocessing
sind beides Bibliotheken zur Implementierung von Multi-Process-Konkurrenz in Python. Es gibt einige Unterschiede:
- Schnittstellenbasierte Kapselung:
concurrent.futures.ProcessPoolExecutor
ist eine High-Level-Schnittstelle, die vom Modulconcurrent.futures
bereitgestellt wird. Es kapselt die zugrunde liegenden Multi-Process-Funktionen, wodurch es einfacher ist, Multi-Process-Code zu schreiben. Währendmultiprocessing
eine der Standardbibliotheken von Python ist, die vollständige Multi-Process-Unterstützung bietet und den direkten Betrieb von Prozessen ermöglicht. - API-Nutzung: Die Verwendung von
concurrent.futures.ProcessPoolExecutor
ähnelt der eines Thread-Pools. Es übermittelt aufrufbare Objekte (wie Funktionen) zur Ausführung an den Prozesspool und gibt einFuture
-Objekt zurück, das zum Abrufen des Ausführungsergebnisses verwendet werden kann.multiprocessing
bietet Low-Level-Prozessverwaltungs- und Kommunikationsschnittstellen. Prozesse können explizit erstellt, gestartet und gesteuert werden, und die Kommunikation zwischen mehreren Prozessen kann mithilfe von Warteschlangen oder Pipes erfolgen. - Skalierbarkeit und Flexibilität: Da
multiprocessing
Low-Level-Schnittstellen bereitstellt, ist es im Vergleich zuconcurrent.futures.ProcessPoolExecutor
flexibler. Durch den direkten Betrieb von Prozessen kann eine detailliertere Steuerung für jeden Prozess erreicht werden, z. B. das Festlegen von Prozessprioritäten und das Austauschen von Daten zwischen Prozessen.concurrent.futures.ProcessPoolExecutor
eignet sich besser für die einfache Aufgabenparallelisierung, blendet viele zugrunde liegende Details aus und erleichtert das Schreiben von Multi-Process-Code. - Plattformübergreifende Unterstützung: Sowohl
concurrent.futures.ProcessPoolExecutor
als auchmultiprocessing
bieten plattformübergreifende Multi-Process-Unterstützung und können auf verschiedenen Betriebssystemen verwendet werden.
Zusammenfassend ist concurrent.futures.ProcessPoolExecutor
eine High-Level-Schnittstelle, die die zugrunde liegenden Multi-Process-Funktionen kapselt und sich für die einfache Multi-Process-Aufgabenparallelisierung eignet. multiprocessing
ist eine Low-Level-Bibliothek, die mehr Kontrolle und Flexibilität bietet und sich für Szenarien eignet, die eine detaillierte Steuerung von Prozessen erfordern. Sie müssen die geeignete Bibliothek entsprechend den spezifischen Anforderungen auswählen. Wenn es sich nur um eine einfache Aufgabenparallelisierung handelt, können Sie concurrent.futures.ProcessPoolExecutor
verwenden, um den Code zu vereinfachen. Wenn mehr Low-Level-Steuerung und -Kommunikation erforderlich sind, können Sie die Bibliothek multiprocessing
verwenden.
Zusammenfassung
Im Gegensatz zu Multithreading ist asyncio
Single-Threaded, aber der Mechanismus seiner internen Ereignisschleife ermöglicht es, mehrere verschiedene Aufgaben gleichzeitig auszuführen und hat eine größere autonome Steuerung als Multithreading.
Aufgaben in asyncio
werden während des Betriebs nicht unterbrochen, sodass die Situation einer Race Condition nicht auftritt.
Insbesondere in Szenarien mit umfangreichen I/O-Operationen hat asyncio
eine höhere Betriebseffizienz als Multithreading. Da die Kosten für den Aufgabenwechsel in asyncio
viel geringer sind als die für den Thread-Wechsel und die Anzahl der Aufgaben, die asyncio
starten kann, viel größer ist als die Anzahl der Threads im Multithreading.
Es ist jedoch zu beachten, dass die Verwendung von asyncio
in vielen Fällen die Unterstützung bestimmter Bibliotheken von Drittanbietern erfordert, wie z. B. aiohttp
im vorherigen Beispiel. Und wenn die I/O-Operationen schnell und nicht aufwändig sind, kann die Verwendung von Multithreading das Problem ebenfalls effektiv lösen.
asyncio
ist eine Python-Bibliothek zur Implementierung asynchroner Programmierung.- Coroutinen sind das Kernkonzept von
asyncio
, das asynchrone Operationen durch die Schlüsselwörterasync
undawait
erreicht. asyncio
bietet eine leistungsstarke API für asynchrone I/O-Operationen und kann I/O-intensive Aufgaben problemlos verarbeiten.- Durch Mechanismen wie
asyncio.gather()
können mehrere Coroutine-Aufgaben gleichzeitig ausgeführt werden.
Leapcell: Die ideale Plattform für FastAPI, Flask und andere Python-Anwendungen
Abschliessend möchte ich die ideale Plattform für die Bereitstellung von Flask/FastAPI vorstellen: Leapcell.
Leapcell ist eine Cloud-Computing-Plattform, die speziell für moderne verteilte Anwendungen entwickelt wurde. Das Pay-as-you-go-Preismodell stellt sicher, dass keine Leerlaufkosten entstehen, d. h. Benutzer zahlen nur für die Ressourcen, die sie tatsächlich nutzen.
- Mehrsprachige Unterstützung
- Unterstützt die Entwicklung in JavaScript, Python, Go oder Rust.
- Kostenlose Bereitstellung unbegrenzter Projekte
- Die Berechnung erfolgt nur nutzungsabhängig. Keine Gebühren, wenn keine Anfragen vorliegen.
- Unübertroffene Kosteneffizienz
- Pay-as-you-go, ohne Leerlaufgebühren.
- Beispielsweise können 25 US-Dollar 6,94 Millionen Anfragen mit einer durchschnittlichen Antwortzeit von 60 Millisekunden unterstützen.
- Vereinfachte Entwicklererfahrung
- Intuitive Benutzeroberfläche für eine einfache Einrichtung.
- Vollständig automatisierte CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokolle, die umsetzbare Einblicke liefern.
- Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Nebenläufigkeit.
- Keine Betriebskosten, sodass sich Entwickler auf die Entwicklung konzentrieren können.
Erfahren Sie mehr in der Dokumentation! Leapcell Twitter: https://x.com/LeapcellHQ