Monkey Patching vs. Async Await: Eine Geschichte zweier Python-Nebenläufigkeitsparadigmen
Wenhao Wang
Dev Intern · Leapcell

Einleitung
Python, eine Sprache, die für ihre Vielseitigkeit und Lesbarkeit gefeiert wird, entwickelt sich ständig weiter, um den Anforderungen der modernen Softwareentwicklung gerecht zu werden. Nebenläufigkeit, die Fähigkeit, mehrere Aufgaben scheinbar gleichzeitig auszuführen, ist ein entscheidender Aspekt beim Aufbau reaktionsschneller und performanter Anwendungen, insbesondere in der heutigen datengesteuerten, netzwerkintensiven Welt. Im Laufe der Jahre haben Python-Entwickler verschiedene Strategien zur Erreichung von Nebenläufigkeit übernommen. Unter diesen haben sich zwei sehr unterschiedliche Philosophien herauskristallisiert: die dynamische, laufzeitverändernde Kraft des Monkey Patching und die strukturiertere, explizite Kontrolle, die die async/await-Syntax bietet. Dieser Artikel befasst sich mit diesen beiden Paradigmen und vergleicht ihre zugrundeliegenden Mechanismen, typischen Anwendungen und die Kompromisse, die sich beim Wählen des einen oder anderen für nebenläufige Operationen in Python ergeben. Das Verständnis dieser Unterschiede ist entscheidend für fundierte Architekturentscheidungen, die die Wartbarkeit, Skalierbarkeit und Robustheit Ihrer Anwendungen beeinflussen.
Die Welt des Monkey Patching und Async/Await
Bevor wir uns dem direkten Vergleich zuwenden, wollen wir ein klares Verständnis der beteiligten Kernkonzepte schaffen.
Kernterminologie
- Nebenläufigkeit: Die Fähigkeit, mehrere Aufgaben gleichzeitig zu bearbeiten. Dies bedeutet nicht unbedingt, dass Aufgaben gleichzeitig ausgeführt werden (Parallelität), sondern dass über einen bestimmten Zeitraum Fortschritte bei mehr als einer Aufgabe erzielt werden können.
- Monkey Patching: Eine Technik zur Erweiterung oder Änderung des Laufzeitcodes eines Programms, ohne dessen ursprünglichen Quellcode zu ändern. Dies beinhaltet normalerweise das Ersetzen von Methoden, Klassen oder sogar ganzen Modulen zur Laufzeit.
async/await: Pythons integrierte Syntax zur Definition und Ausführung von Coroutinen – Funktionen, die pausiert und fortgesetzt werden können. Dieser nicht-blockierende I/O-Ansatz ist das Herzstück der asynchronen Programmierung und ermöglicht es einem einzelnen Thread, mehrere I/O-gebundene Operationen effektiv zu verwalten.- Ereignisschleife (Event Loop): Der Kern von
asynciound ähnlichen asynchronen Frameworks. Sie ist für die Planung und Ausführung von Coroutinen, die Verwaltung von I/O-Operationen und die Verteilung von Ereignissen zuständig. - Coroutinen: Spezielle Funktionen in Python (definiert mit
async def), die ihre Ausführung pausieren und die Kontrolle an eine Ereignisschleife zurückgeben können, wodurch andere Aufgaben ausgeführt werden können. Wenn die I/O-Operation, auf die sie gewartet haben, abgeschlossen ist, können sie fortgesetzt werden.
Monkey Patching für Nebenläufigkeit
Monkey Patching für Nebenläufigkeit beinhaltet typischerweise Bibliotheken wie gevent oder eventlet. Diese Bibliotheken erreichen Nebenläufigkeit, indem sie Standardbibliotheksfunktionen (z. B. I/O-Operationen wie socket oder time.sleep) so patchen, dass sie "kooperativ geplant" werden. Wenn eine gepatchte I/O-Operation aufgerufen wird, gibt sie anstatt den gesamten Thread zu blockieren die Kontrolle an den gevent- oder eventlet-Scheduler ab, der dann zu einem anderen "Greenlet" (einer von diesen Bibliotheken bereitgestellten leichten Coroutine) wechselt.
So funktioniert es:
- Importieren und "Monkey Patching" der Standardbibliothek:
# gevent Beispiel from gevent import monkey monkey.patch_all() # Patched Standardbibliotheksmodule wie socket, ssl, threading, time usw. import gevent import requests # Verwendet nun unter der Haube gevent-fähige Sockets - Definieren Sie Funktionen, die I/O-Operationen wie gewohnt ausführen. Es werden keine speziellen
async- oderawait-Schlüsselwörter benötigt. - Verwenden Sie
gevent.spawnodereventlet.spawn, um Greenlets zu erstellen, undgevent.joinall, um auf sie zu warten.
Beispiel: Mehrere URLs gleichzeitig mit gevent abrufen:
# gevent_example.py from gevent import monkey import gevent import time import requests # Standardbibliothek patchen (z. B. socket, time) monkey.patch_all() def fetch_url(url): print(f"Starting to fetch: {url}") try: response = requests.get(url) print(f"Finished fetching: {url}, Status: {response.status_code}") return len(response.content) except Exception as e: print(f"Error fetching {url}: {e}") return 0 urls = [ "http://www.google.com", "http://www.yahoo.com", "http://www.bing.com", "http://www.python.org", ] if __name__ == "__main__": start_time = time.time() # Greenlets für jede URL spawnen geenlets = [gevent.spawn(fetch_url, url) for url in urls] # Auf den Abschluss aller Greenlets warten gevent.joinall(geenlets) total_bytes = sum(g.value for g in geenlets) end_time = time.time() print(f"\nTotal bytes fetched: {total_bytes}") print(f"Total time taken: {end_time - start_time:.2f} seconds")
In diesem Beispiel blockiert requests.get() normalerweise. Nachdem jedoch monkey.patch_all() aufgerufen wurde, werden die zugrundeliegenden Socket-Operationen nicht-blockierend und geben die Kontrolle ab, sodass andere fetch_url-Aufrufe im selben Thread gleichzeitig fortgesetzt werden können.
Anwendungsszenarien:
- Migration bestehender blockierender Codes zu einem nebenläufigen Modell mit minimalen Codeänderungen.
- Integration mit Legacy-Bibliotheken, die
async/awaitnicht unterstützen. - Webserver (wie Gunicorn mit
gevent- odereventlet-Workern), um viele nebenläufige Verbindungen zu verarbeiten.
Die async/await-Welt
async/await ist Pythons explizite native Unterstützung für asynchrone Programmierung, eingeführt in Python 3.5 mit der asyncio-Bibliothek. Sie basiert auf dem Prinzip des kooperativen Multitasking mittels einer Ereignisschleife. Funktionen, die mit async def markiert sind, sind Coroutinen und weisen explizit Punkte an, an denen sie ihre Ausführung mit dem Schlüsselwort await pausieren können.
So funktioniert es:
- Definieren Sie Coroutinen mit
async def. - Verwenden Sie innerhalb einer Coroutine
await, um die Ausführung zu pausieren, bis einawaitable(eine andere Coroutine, ein Future oder eine Aufgabe) abgeschlossen ist. - Eine Ereignisschleife führt diese Coroutinen aus und wechselt zwischen ihnen, wenn eine auf eine I/O-Operation wartet.
Beispiel: Mehrere URLs gleichzeitig mit asyncio und httpx abrufen:
# asyncio_example.py import asyncio import httpx # Ein async-fähiger HTTP-Client import time async def fetch_url_async(client, url): print(f"Starting to fetch: {url}") try: response = await client.get(url) # Den asynchronen HTTP GET-Request awaiten print(f"Finished fetching: {url}, Status: {response.status_code}") return len(response.content) except Exception as e: print(f"Error fetching {url}: {e}") return 0 async def main(): urls = [ "http://www.google.com", "http://www.yahoo.com", "http://www.bing.com", "http://www.python.org", ] start_time = time.time() async with httpx.AsyncClient() as client: # Einen asynchronen HTTP-Client verwenden tasks = [fetch_url_async(client, url) for url in urls] # Aufgaben gleichzeitig ausführen und auf den Abschluss aller warten results = await asyncio.gather(*tasks) total_bytes = sum(results) end_time = time.time() print(f"\nTotal bytes fetched: {total_bytes}") print(f"Total time taken: {end_time - start_time:.2f} seconds") if __name__ == "__main__": asyncio.run(main()) # Die Haupt-Coroutine ausführen
Hier ist httpx.AsyncClient().get() ein awaitable. Wenn await client.get(url) aufgerufen wird, pausiert fetch_url_async, und die Ereignisschleife kann zu anderen ausstehenden Aufgaben wechseln oder gleichzeitig neue get-Anfragen starten.
Anwendungsszenarien:
- Erstellung hochperformanter Netzwerkanwendungen (Webserver, APIs, Chat-Anwendungen).
- Datenbanken und Microservices, die andere asynchrone Dienste konsumieren.
- Jede I/O-gebundene Anwendung, bei der eine explizite Steuerung des Nebenläufigkeitsflusses gewünscht wird.
- Moderne Web-Frameworks wie FastAPI und Starlette basieren vollständig auf
asyncio.
Vergleich und Kompromisse
| Merkmal | Monkey Patching (z. B. gevent) | Async/Await (asyncio) |
|---|---|---|
| Explizitheit | Implizit: Standard-Blockierungsaufrufe werden nicht-blockierend. | Explizit: async- und await-Schlüsselwörter kennzeichnen Nebenläufigkeit klar. |
| Invasivität | Stark invasiv: Modifiziert den globalen Zustand und die Standardbibliothek zur Laufzeit. | Nicht-invasiv: Basiert auf expliziten async-APIs. |
| Lernkurve | Niedriger für bestehenden blockierenden Code, aber das Verständnis von Patching kann komplex sein. | Höher für Anfänger, erfordert Verständnis von Ereignisschleifen und Coroutinen. |
| Ökosystem-Support | Nische, basiert auf gepatchten Versionen von Bibliotheken. Nicht alle Bibliotheken sind patchbar. | Wächst rasant, viele moderne Bibliotheken sind nativ asyncio-fähig. |
| Debugging | Kann schwierig sein aufgrund des impliziten Kontrollflusses und geänderter Stack-Traces. | Im Allgemeinen klarere Fehlermeldungen und Stack-Traces aufgrund expliziter Steuerung. |
| Performance | Hervorragend für I/O-gebundene Aufgaben in einem einzelnen Thread. Oft ähnlich wie asyncio. | Hervorragend für I/O-gebundene Aufgaben. Native Unterstützung bietet Optimierungspotenzial. |
| Kompatibilität | Kann problematisch sein mit Bibliotheken, die ihre eigene Low-Level-I/O oder Threading durchführen. | Erfordert, dass Bibliotheken asyncio-kompatibel sind oder async-Wrapper bereitstellen. |
| Gedankliches Modell | "Es funktioniert einfach" (bis es das nicht tut). | "Explizit kooperativ". |
Wo Monkey Patching glänzt:
- Migration von Legacy-Code: Wenn Sie eine riesige bestehende Codebasis mit traditionellen blockierenden Aufrufen haben, kann Monkey Patching eine schnelle Möglichkeit bieten, Nebenläufigkeit einzuführen, ohne massive Umschreibungen vornehmen zu müssen.
- Schnelles Prototyping: Um schnell Nebenläufigkeit in eine nicht-nebenläufige Anwendung zu integrieren, bei der der Aufwand für die Neufassung für
asyncioals zu hoch eingeschätzt wird.
Wo Async/Await glänzt:
- Neue Projekte: Zum Erstellen moderner, hochperformanter Anwendungen von Grund auf, bei denen ein explizites Nebenläufigkeitsmodell bevorzugt wird.
- Robustheit und Wartbarkeit: Die Explizitheit von
async/awaitmacht den Code leichter nachvollziehbar, verständlich und debuggbar, was auf lange Sicht zu wartbareren Anwendungen führt. - Modernes Ökosystem: Nahtlose Integration mit dem wachsenden Ökosystem von
asyncio-nativen Bibliotheken und Frameworks. - Klarer Kontrollfluss: Entwickler haben eine feinere Kontrolle darüber, wann Kontextwechsel stattfinden.
Fazit
Sowohl Monkey Patching (hauptsächlich durch Bibliotheken wie gevent) als auch async/await bieten leistungsstarke Möglichkeiten, Nebenläufigkeit in Python zu erreichen. Monkey Patching bietet einen fast magischen Übergang für bestehenden blockierenden Code, der es ermöglicht, diesen mit minimalen Syntaxänderungen nebenläufig auszuführen, jedoch auf Kosten der globalen Zustandsänderung und potenzieller Debugging-Schwierigkeiten. Im Gegensatz dazu erfordert async/await einen expliziteren und bewussteren Ansatz für die nebenläufige Programmierung, der asynchrone Operationen klar kennzeichnet und zu robusteren, wartbareren und moderneren Code führt, insbesondere für neue Projekte oder vollständige Neufassungen. Die Wahl zwischen diesen beiden Paradigmen hängt von den spezifischen Projektanforderungen, der bestehenden Codebasis und der Komfortstufe des Teams mit expliziter asynchroner Programmierung ab, aber der Trend geht eindeutig zum expliziten Kontrollfluss und dem wachsenden Ökosystem von async/await. Für Greenfield-Projekte ist die Umarmung von async/await zweifellos der Weg nach vorn für skalierbare und transparente nebenläufige Python-Anwendungen.

