3 Schritte zu einer asynchronen Python-Überholung

Python ist eine von vielen Sprachen, die das Schreiben asynchroner Programme unterstützen - Programme, die frei zwischen mehreren Aufgaben wechseln und alle gleichzeitig ausgeführt werden, sodass keine Aufgabe den Fortschritt der anderen aufhält.

Es besteht jedoch die Möglichkeit, dass Sie hauptsächlich synchrone Python-Programme geschrieben haben - Programme, die jeweils nur eine Aufgabe ausführen und darauf warten, dass jede Aufgabe abgeschlossen ist, bevor Sie eine andere starten. Der Wechsel zu Async kann erschütternd sein, da nicht nur neue Syntax, sondern auch neue Denkweisen über den eigenen Code erlernt werden müssen. 

In diesem Artikel werden wir untersuchen, wie ein vorhandenes synchrones Programm in ein asynchrones umgewandelt werden kann. Dies beinhaltet mehr als nur das Dekorieren von Funktionen mit asynchroner Syntax. Dazu muss man auch anders darüber nachdenken, wie unser Programm abläuft, und entscheiden, ob Async überhaupt eine gute Metapher für das ist, was es tut. 

[Ebenfalls am: Erfahren Sie Python-Tipps und Tricks aus den Smart Python-Videos von Serdar Yegulalp]

Wann wird Async in Python verwendet?

Ein Python-Programm eignet sich am besten für Async, wenn es die folgenden Eigenschaften aufweist:

  • Es wird versucht, etwas zu tun, das hauptsächlich durch E / A gebunden ist, oder indem darauf gewartet wird, dass ein externer Prozess abgeschlossen wird, z. B. ein lang laufender Netzwerklesevorgang.
  • Es wird versucht, eine oder mehrere dieser Aufgaben gleichzeitig zu erledigen und möglicherweise auch Benutzerinteraktionen zu verarbeiten.
  • Die fraglichen Aufgaben sind nicht rechenintensiv.

Ein Python-Programm, das Threading verwendet, ist normalerweise ein guter Kandidat für die Verwendung von Async. Threads in Python sind kooperativ. sie geben einander nach Bedarf nach. Asynchrone Aufgaben in Python funktionieren genauso. Außerdem bietet Async bestimmte Vorteile gegenüber Threads:

  • Die async/ await-Syntax erleichtert die Identifizierung der asynchronen Teile Ihres Programms. Im Gegensatz dazu ist es oft schwierig, auf einen Blick zu erkennen, welche Teile einer App in einem Thread ausgeführt werden. 
  • Da asynchrone Aufgaben denselben Thread verwenden, werden alle Daten, auf die sie zugreifen, automatisch von der GIL verwaltet (Pythons nativer Mechanismus zum Synchronisieren des Zugriffs auf Objekte). Threads erfordern häufig komplexe Mechanismen für die Synchronisation. 
  • Asynchrone Aufgaben sind einfacher zu verwalten und abzubrechen als Threads.

Die Verwendung von Async wird nicht empfohlen, wenn Ihr Python-Programm folgende Eigenschaften aufweist:

  • Die Aufgaben haben einen hohen Rechenaufwand - z. B. führen sie eine starke Zahlenkalkulation durch. Schwere Rechenarbeit wird am besten erledigt multiprocessing, sodass Sie jeder Aufgabe einen ganzen Hardwarethread widmen können .
  • Die Aufgaben profitieren nicht von einer Verschachtelung. Wenn jede Aufgabe von der letzten abhängt, macht es keinen Sinn, sie asynchron auszuführen. Das heißt, wenn das Programm  Sätze von seriellen Aufgaben umfasst, können Sie jeden Satz asynchron ausführen.

Schritt 1: Identifizieren Sie die synchronen und asynchronen Teile Ihres Programms

Asynchroner Python-Code muss von den synchronen Teilen Ihrer Python-Anwendung gestartet und verwaltet werden. Zu diesem Zweck besteht Ihre erste Aufgabe beim Konvertieren eines Programms in Async darin, eine Linie zwischen den synchronen und asynchronen Teilen Ihres Codes zu ziehen.

In unserem vorherigen Artikel über Async haben wir eine Web-Scraper-App als einfaches Beispiel verwendet. Die asynchronen Teile des Codes sind die Routinen, die die Netzwerkverbindungen öffnen und von der Site lesen - alles, was Sie verschachteln möchten. Aber der Teil des Programms, der all das startet, ist nicht asynchron. Es startet die asynchronen Aufgaben und schließt sie dann ordnungsgemäß ab, wenn sie fertig sind.

Es ist auch wichtig, potenziell blockierende Vorgänge von asynchron zu trennen  und im Synchronisierungsteil Ihrer App zu belassen. Das Lesen von Benutzereingaben von der Konsole blockiert beispielsweise alles, einschließlich der asynchronen Ereignisschleife. Daher möchten Sie Benutzereingaben entweder vor dem Starten von asynchronen Aufgaben oder nach deren Abschluss verarbeiten. (Es ist möglich, Benutzereingaben asynchron über Multiprocessing oder Threading zu verarbeiten, aber das ist eine fortgeschrittene Übung, auf die wir hier nicht eingehen werden.)

Einige Beispiele für Blockierungsvorgänge:

  • Konsoleneingang (wie gerade beschrieben).
  • Aufgaben mit hoher CPU-Auslastung.
  • Verwenden time.sleep, um eine Pause zu erzwingen. Beachten Sie, dass Sie in einer asynchronen Funktion schlafen können, indem Sie asyncio.sleepals Ersatz für verwenden time.sleep.

Schritt 2: Konvertieren Sie die entsprechenden Synchronisierungsfunktionen in asynchrone Funktionen

Sobald Sie wissen, welche Teile Ihres Programms asynchron ausgeführt werden, können Sie sie in Funktionen aufteilen (falls Sie dies noch nicht getan haben) und sie mit dem asyncSchlüsselwort in asynchrone Funktionen umwandeln . Anschließend müssen Sie dem synchronen Teil Ihrer Anwendung Code hinzufügen, um den asynchronen Code auszuführen und bei Bedarf Ergebnisse daraus zu sammeln.

Hinweis: Sie sollten die Aufrufkette jeder asynchronen Funktion überprüfen und sicherstellen, dass sie keinen möglicherweise lang laufenden oder blockierenden Vorgang aufruft. Asynchrone Funktionen können Synchronisierungsfunktionen direkt aufrufen. Wenn diese Synchronisierungsfunktion blockiert, ruft auch die asynchrone Funktion sie auf.

Schauen wir uns ein vereinfachtes Beispiel an, wie eine Synchronisierung in eine asynchrone Konvertierung funktionieren könnte. Hier ist unser Vorher-Programm:

def a_function (): # eine asynchrone kompatible Aktion, die eine Weile dauert def another_function (): # eine Synchronisierungsfunktion, aber keine blockierende def do_stuff (): a_function () another_function () def main (): für _ in range (3): do_stuff () main () 

Wenn drei Instanzen do_stuffals asynchrone Aufgaben ausgeführt werden sollen, müssen wir do_stuff (und möglicherweise alles, was sie berühren) in asynchronen Code umwandeln. Hier ist ein erster Durchgang bei der Konvertierung:

asyncio importieren async def a_function (): # eine async-kompatible Aktion, die eine Weile dauert def another_function (): # eine Synchronisierungsfunktion, aber keine blockierende async def do_stuff (): warte auf a_function () another_function () async def main () ): Aufgaben = [] für _ in Bereich (3): Aufgaben.append (asyncio.create_task (do_stuff ())) warten auf asyncio.gather (Aufgaben) asyncio.run (main ()) 

Beachten Sie die Änderungen, die wir vorgenommen haben  main. Wird jetzt main verwendet asyncio, um jede Instanz von do_stuffals gleichzeitige Aufgabe zu starten , und wartet dann auf die Ergebnisse ( asyncio.gather). Wir haben auch a_functionin eine asynchrone Funktion konvertiert , da alle Instanzen a_functionnebeneinander und neben allen anderen Funktionen, die asynchrones Verhalten erfordern, ausgeführt werden sollen.

Wenn wir noch einen Schritt weiter gehen wollten, könnten wir auch another_functionzu async konvertieren :

async def another_function (): # eine Synchronisierungsfunktion, aber keine blockierende async def do_stuff (): warte auf eine_Funktion () warte auf eine andere_Funktion () 

Allerdings macht  another_function viel des Guten wäre asynchron, da (wie wir festgestellt haben) es nichts tut, die den Fortschritt unseres Programms blockieren würden. Wenn synchrone Teile unseres Programms aufgerufen werden  another_function, müssen wir sie ebenfalls in asynchron konvertieren, was unser Programm komplizierter machen könnte, als es sein muss.

Schritt 3: Testen Sie Ihr Python-Async-Programm gründlich

Jedes asynchron konvertierte Programm muss vor Produktionsbeginn getestet werden, um sicherzustellen, dass es wie erwartet funktioniert.

If your program is modest in size — say, a couple of dozen lines or so — and doesn’t need a full test suite, then it shouldn’t be difficult to verify that it works as intended. That said, if you’re converting the program to async as part of a larger project, where a test suite is a standard fixture, it makes sense to write unit tests for async and sync components alike.

Both of the major test frameworks in Python now feature some kind of async support. Python’s own unittest framework includes test case objects for async functions, and pytest offers pytest-asyncio for the same ends.

Finally, when writing tests for async components, you’ll need to handle their very asynchronousness as a condition of the tests. For instance, there is no guarantee that async jobs will complete in the order they were submitted. The first one might come in last, and some might never complete at all. Any tests you design for an async function must take these possibilities into account.

How to do more with Python

  • Get started with async in Python
  • How to use asyncio in Python
  • How to use PyInstaller to create Python executables
  • Cython tutorial: How to speed up Python
  • How to install Python the smart way
  • How to manage Python projects with Poetry
  • How to manage Python projects with Pipenv
  • Virtualenv and venv: Python virtual environments explained
  • Python virtualenv and venv do’s and don’ts
  • Python threading and subprocesses explained
  • How to use the Python debugger
  • Verwendung von timeit zum Profilieren von Python-Code
  • Verwendung von cProfile zum Profilieren von Python-Code
  • So konvertieren Sie Python in JavaScript (und wieder zurück)