Python 3.5 führte asynchrone E/A als Alternative zu Threads zur Handhabung der Parallelität ein. Der Vorteil der asynchronen E/A und der Asyncio-Implementierung in Python besteht darin, dass das System weniger Ressourcen verbraucht und skalierbarer ist, da keine speicherintensiven Betriebssystem-Threads erzeugt werden. Darüber hinaus werden bei Asyncio Planungspunkte klar über die await
-Syntax definiert, während bei Thread-basierter Parallelität die GIL an unvorhersehbaren Codepunkten freigegeben werden kann. Dadurch sind Asyncio-basierte Parallelitätssysteme einfacher zu verstehen und zu debuggen. Schließlich kann die Asyncio-Aufgabe abgebrochen werden, was bei der Verwendung von Threads nicht einfach ist.
Um diese Vorteile wirklich nutzen zu können, ist es jedoch wichtig, das Blockieren von Aufrufen in asynchronen Coroutinen zu vermeiden. Blockierende Aufrufe können Netzwerkaufrufe, Dateisystemaufrufe, sleep
-Aufrufe usw. sein. Diese blockierenden Aufrufe sind schädlich, da Asyncio unter der Haube eine Single-Thread-Ereignisschleife verwendet, um Coroutinen gleichzeitig auszuführen. Wenn Sie also einen blockierenden Aufruf in einer Coroutine durchführen, blockiert dieser die gesamte Ereignisschleife und alle Coroutinen, was sich auf die Gesamtleistung Ihrer Anwendung auswirkt.
Das Folgende ist ein Beispiel für einen Blockierungsaufruf, der verhindert, dass Code gleichzeitig ausgeführt wird:
import asyncio import datetime import time async def example(name): print(f"{datetime.datetime.now()}: {name} start") time.sleep(1) # time.sleep 是一个阻塞函数 print(f"{datetime.datetime.now()}: {name} stop") async def main(): await asyncio.gather(example("1"), example("2")) asyncio.run(main())
Das Laufergebnis ist ähnlich wie:
<code>2025-01-07 18:50:15.327677: 1 start 2025-01-07 18:50:16.328330: 1 stop 2025-01-07 18:50:16.328404: 2 start 2025-01-07 18:50:17.333159: 2 stop</code>
Wie Sie sehen können, laufen die beiden Coroutinen nicht gleichzeitig.
Um dieses Problem zu lösen, müssen Sie ein nicht blockierendes Äquivalent verwenden oder die Ausführung auf den Thread-Pool verschieben:
import asyncio import datetime import time async def example(name): print(f"{datetime.datetime.now()}: {name} start") await asyncio.sleep(1) # 将阻塞的 time.sleep 调用替换为非阻塞的 asyncio.sleep 协程 print(f"{datetime.datetime.now()}: {name} stop") async def main(): await asyncio.gather(example("1"), example("2")) asyncio.run(main())
Das Laufergebnis ist ähnlich wie:
<code>2025-01-07 18:53:53.579738: 1 start 2025-01-07 18:53:53.579797: 2 start 2025-01-07 18:53:54.580463: 1 stop 2025-01-07 18:53:54.580572: 2 stop</code>
Hier laufen zwei Coroutinen gleichzeitig.
Das Problem besteht nun darin, dass es nicht immer einfach ist zu erkennen, ob eine Methode blockiert oder nicht. Vor allem, wenn die Codebasis groß ist oder Bibliotheken von Drittanbietern verwendet. Manchmal werden blockierende Aufrufe in tiefen Teilen des Codes durchgeführt.
Blockiert dieser Code beispielsweise?
import blockbuster from importlib.metadata import version async def get_version(): return version("blockbuster")
Lädt Python beim Start Paketmetadaten in den Speicher? Ist dies erledigt, wenn das Modul blockbuster
geladen wird? Oder wenn wir version()
anrufen? Werden die Ergebnisse zwischengespeichert und sind nachfolgende Aufrufe nicht blockierend? Die richtige Antwort erfolgt beim Aufruf von version()
, was das Lesen der METADATA-Datei des installierten Pakets beinhaltet. Und die Ergebnisse werden nicht zwischengespeichert. Daher ist version()
ein blockierender Aufruf und sollte immer an den Thread verschoben werden. Es ist schwer, diese Tatsache zu erkennen, ohne sich mit dem Code von importlib
auseinanderzusetzen.
Eine Möglichkeit, blockierende Aufrufe zu erkennen, besteht darin, den Debug-Modus von Asyncio zu aktivieren, um blockierende Aufrufe zu protokollieren, die zu lange dauern. Dies ist jedoch nicht der effizienteste Ansatz, da viele Blockierungszeiten, die kürzer als das Trigger-Timeout sind, immer noch die Leistung beeinträchtigen und die Blockierungszeiten im Test/in der Entwicklung möglicherweise anders sind als in der Produktion. Beispielsweise können Datenbankaufrufe in einer Produktionsumgebung länger dauern, wenn die Datenbank große Datenmengen abrufen muss.
Hier kommt BlockBuster ins Spiel! Bei Aktivierung patcht BlockBuster mehrere blockierende Python-Framework-Methoden, die Fehler auslösen, wenn sie aus der Asyncio-Ereignisschleife aufgerufen werden. Zu den Standard-Patching-Methoden gehören Methoden der Module os
, io
, time
, socket
und sqlite
. Eine vollständige Liste der von BlockBuster erkannten Methoden finden Sie in der Projekt-Readme-Datei. Anschließend können Sie BlockBuster im Unit-Test- oder Entwicklungsmodus aktivieren, um blockierende Aufrufe abzufangen und zu beheben. Wenn Sie die großartige BlockHound-Bibliothek für die JVM kennen, ist es das gleiche Prinzip, aber für Python. BlockHound war dank der Macher eine großartige Inspirationsquelle für BlockBuster.
Sehen wir uns an, wie Sie BlockBuster für das obige Blockierungscode-Snippet verwenden.
Zuerst müssen wir das blockbuster
Paket
import asyncio import datetime import time async def example(name): print(f"{datetime.datetime.now()}: {name} start") time.sleep(1) # time.sleep 是一个阻塞函数 print(f"{datetime.datetime.now()}: {name} stop") async def main(): await asyncio.gather(example("1"), example("2")) asyncio.run(main())
Wir können dann das Pytest-Fixture und die blockbuster_ctx()
-Methode verwenden, um den BlockBuster zu Beginn jedes Tests zu aktivieren und ihn während des Teardowns zu deaktivieren.
<code>2025-01-07 18:50:15.327677: 1 start 2025-01-07 18:50:16.328330: 1 stop 2025-01-07 18:50:16.328404: 2 start 2025-01-07 18:50:17.333159: 2 stop</code>
Wenn Sie dies mit Pytest ausführen, erhalten Sie
import asyncio import datetime import time async def example(name): print(f"{datetime.datetime.now()}: {name} start") await asyncio.sleep(1) # 将阻塞的 time.sleep 调用替换为非阻塞的 asyncio.sleep 协程 print(f"{datetime.datetime.now()}: {name} stop") async def main(): await asyncio.gather(example("1"), example("2")) asyncio.run(main())
Hinweis: Typischerweise wird in einem realen Projekt das
blockbuster()
-Gerät in einerconftest.py
-Datei eingerichtet.
Ich glaube, dass BlockBuster in Asyncio-Projekten sehr nützlich ist. Es hat mir geholfen, viele Probleme mit blockierenden Anrufen in Projekten, an denen ich gearbeitet habe, zu erkennen. Aber es ist kein Allheilmittel. Insbesondere verwenden einige Bibliotheken von Drittanbietern keine Python-Framework-Methoden für die Interaktion mit dem Netzwerk oder Dateisystem, sondern umschließen stattdessen C-Bibliotheken. Für diese Bibliotheken können Sie in Ihrem Test-Setup Regeln hinzufügen, um blockierende Aufrufe an diese Bibliotheken auszulösen. BlockBuster ist ebenfalls Open Source: Beiträge sind herzlich willkommen, um Regeln für Ihre Lieblingsbibliotheken im Kernprojekt hinzuzufügen. Wenn Sie Probleme und Verbesserungsmöglichkeiten sehen, würde ich mich über Ihr Feedback im Projekt-Issue-Tracker freuen.
Einige Links:
Das obige ist der detaillierte Inhalt vonEinführung in BlockBuster: Ist meine Asyncio-Ereignisschleife blockiert?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!