async/await in Python: Nebenläufigkeit leicht gemacht

Mit überschaubarem Aufwand, und ohne auf komplizierte Threading-Mechanismen zurückgreifen zu müssen, lassen sich Programme nebenläufig entwickeln.

In Pocket speichern vorlesen Druckansicht 12 Kommentare lesen

(Bild: rodho/Shutterstock.com)

Lesezeit: 13 Min.
Von
  • Martin Meeser
Inhaltsverzeichnis

Das mächtige Konstrukt async/await stellt Python bereits seit vielen Jahren zur Verfügung. Coroutines mit den Schlüsselwörtern async und await hielten 2015 in Python 3.5 Einzug. Damit lassen sich nahezu ohne Mehraufwand nebenläufige Programme entwickeln. Mit etwas Hintergrundwissen eröffnen sich darüber hinaus Möglichkeiten, komplexe nebenläufige Programme elegant zu entwerfen, ohne dafür komplizierte Threading-Mechanismen kennen oder gar anwenden zu müssen.

Eine asynchrone Funktions-Definition wird englisch als Coroutine – eine kooperative Routine – bezeichnet. Um sie zu verwenden, muss dem def-Schlüsselwort das async-Schlüsselwort vorangestellt sein:

async def af():
	i = await y()
	print(i)
	print(await y())

Listing 1: Definition einer Coroutine

Der Grundgedanke dabei ist, dass eine kooperative Funktion (Routine ist nur ein anderer Begriff für Funktion) von sich aus anderen Programmteilen anbietet, die eigene Ausführung zu unterbrechen, und dadurch Rechen- und Ressourcen-Kapazitäten zur Verfügung stellt.

Eine async def kann an all jenen Stellen definiert werden, an denen man eine standardmäßige def verwenden kann – beispielsweise auch innerhalb von Klassen:

class MyClass:
	async def caf(self):
		i = await y()
		print(i)
		print(await y())

Listing 2: Definition einer Coroutine als Klassen-Methode

Das await-Schlüsselwort darf, wie in den obigen Beispielen bereits zu sehen ist, ausschließlich innerhalb von Coroutines zum Einsatz kommen. An den durch await gekennzeichneten Stellen ist die Coroutine kooperativ: Hier lässt sich das Ausführen der Funktion unterbrechen. Sobald das Ergebnis der asynchronen Operation vorliegt (im Beispiel await y()), wird das Abarbeiten der Funktion fortgesetzt. Es kommt an dieser Stelle aber nicht zu einem Blockieren des Threads.

Innerhalb einer Coroutine darf es auch Anweisungen ohne await geben. Eine Coroutine ohne await ist technisch möglich, aber nicht sinnvoll.

Eine Coroutine kann nur innerhalb einer Event-Loop ausgeführt werden, die sich mit der Funktion asyncio.run aus dem Modul asyncio erzeugen lässt. Das empfohlene Vorgehen dabei ist es, eine asynchrone main-Funktion zu definieren und den gesamten Programmablauf direkt an die Event-Loop zu übergeben:

async def main():
	pass # non blocking program

if __name__ == "__main__":
	asyncio.run(main())

Listing 3: Main-Methode in einem Python-Programm, in dem async/await zum Einsatz kommt

Die Event-Loop endet, sobald keine asynchronen Aufgaben mehr vorliegen. Je Thread kann es nur eine Event-Loop geben. Entsprechend sollte man die Funktion asyncio.run nur einmal pro Thread aufrufen.

Klassische Programme haben ein wesentliches Problem: Sie blockieren, wenn sie auf Ressourcen wie Speicher oder Netzwerk zugreifen. Man betrachte dabei folgendes Beispiel:

def main():
	with open('file.txt', 'r') as file:
		s: str = file.readline()

Listing 4: Blockierendes Öffnen einer Datei

Was genau passiert hier? Das Python-Programm läuft innerhalb eines CPython-Prozesses auf dem Betriebssystem. Um eine Datei zu öffnen und aus ihr zu lesen, muss CPython Anfragen an das Betriebssystem senden. Jenes prüft unter anderem, ob die Datei existiert. Dabei kann etwas Zeit vergehen, beispielsweise, weil eine mechanische Festplatte erst anlaufen muss oder ein Netzlaufwerk die Gegenstelle kontaktiert. Der Aufruf an open kehrt erst zurück, nachdem die Operation vollständig ausgeführt wurde.

Konzeptuelle Darstellung des blockierenden Öffnens (Abb. 1)

Während der Thread aus dem CPython-Programm auf die Rückmeldung des Betriebssystems wartet, ist er blockiert – kann also keine weiteren Aktionen oder Berechnungen ausführen. In dieser Zeit stoppt der Programmfortschritt.