Python Async: Warum ist es noch nicht im Mainstream angekommen?
(tonybaloney.github.io)Python Async: Warum ist es noch nicht im Mainstream angekommen?
asyncio in Python ist ein leistungsstarkes Werkzeug, das in Umgebungen mit vielen I/O-(Ein-/Ausgabe-)Operationen Wartezeiten reduzieren und die Effizienz von Programmen drastisch steigern kann. Trotz dieser Vorteile wird es jedoch nicht von allen Python-Entwicklern aktiv genutzt – dafür gibt es einige grundlegende Gründe.
1. Es macht den Kopf voll: kognitive Last
Die größte Hürde ist die Komplexität. Synchroner Code ist intuitiv, weil wir dem Ausführungsfluss wie beim Lesen eines Buches von oben nach unten der Reihe nach folgen können.
Bei asynchronem Code ist das anders. Es ist, als würde ein Koch Pasta zubereiten und während die Nudeln kochen (Wartezeit), gleichzeitig die Sauce machen und in der übrigen Zeit Gemüse schneiden. Am Ende ist das Essen zwar schneller fertig, aber im Kopf des Kochs laufen ständig Fragen mit wie: „Wie gar sind die Nudeln gerade?“ oder „Brennt die Sauce an?“.
Weil der Ausführungsfluss des Codes so ständig zwischen verschiedenen Stellen springt, ist es schwer zu erkennen, welche Aufgabe gerade läuft und welche wartet. Das macht insbesondere den Debugging-Prozess bei Fehlern sehr anspruchsvoll.
2. Ein getrenntes Ökosystem: Bibliothekskompatibilität
Ein weiteres Problem in Python ist, dass das Bibliotheksökosystem in „synchron“ und „asynchron“ aufgeteilt ist. Viele bekannte und praktische Bibliotheken (z. B. die Standardbibliothek für HTTP-Anfragen requests oder viele ORMs) arbeiten nur synchron.
Wenn in asynchronem Code versehentlich eine synchrone Bibliothek verwendet wird, stoppt dieser synchrone Code während seiner Laufzeit den „Event Loop“ – also genau den größten Vorteil von Async. Das ist, als würde man mehrere Fahrspuren bauen, aber nur eine davon nutzen; der Einsatz von Async verliert damit seinen Sinn. Um dieses Problem zu lösen, muss man separate Bibliotheken mit Async-Unterstützung (aiohttp, asyncpg usw.) neu lernen und einführen – das macht die Lernkurve noch steiler.
3. Immer nur einer gleichzeitig: GIL (Global Interpreter Lock)
Der GIL (Global Interpreter Lock) ist eine bekannte Eigenheit von Python: Innerhalb eines Prozesses kann selbst bei mehreren Threads immer nur genau ein Thread gleichzeitig ausgeführt werden. asyncio arbeitet zwar in einem einzelnen Thread und kollidiert daher nicht direkt mit dem GIL, aber die Existenz des GIL begrenzt dennoch den Einsatzbereich von async.
asyncio ist dafür optimiert, I/O-Wartezeiten zu nutzen (z. B. Warten auf Netzwerkantworten oder Datei-Lesevorgänge). Wenn jedoch in einer async-Funktion eine rechenintensive CPU-bound-Aufgabe enthalten ist, steht der gesamte Event Loop still, bis diese Berechnung abgeschlossen ist. In dieser Zeit können andere I/O-Aufgaben nichts tun und müssen warten. Für Aufgaben, die echte Parallelverarbeitung erfordern, bleibt man daher weiterhin auf andere Techniken wie multiprocessing angewiesen.
4. Hoffnung für die Zukunft: Python 3.14 und die Abschaffung des GIL
Es gibt jedoch sehr ermutigende Nachrichten zu diesen Einschränkungen: nämlich die Bewegung hin zur optionalen Abschaffung des GIL, die ab Python 3.13 experimentell eingeführt wurde und in Version 3.14 voraussichtlich weiter ausreifen wird.
Diese durch den Vorschlag PEP 703 vorangetriebene Änderung zielt darauf ab, dass Entwickler Python-Code bei Bedarf ohne GIL ausführen können. Sollte das Realität werden, wäre in Python echtes Multithreading möglich, bei dem mehrere Threads mehrere CPU-Kerne gleichzeitig nutzen.
Gerade in Kombination mit asyncio könnte das enorme Synergien erzeugen. I/O-Aufgaben ließen sich effizient mit asyncio verarbeiten, während CPU-intensive Berechnungen an separate Threads ausgelagert und ohne die Beschränkungen des GIL parallel ausgeführt werden könnten. Diese Veränderung dürfte einen wichtigen Wendepunkt für das Python-Ökosystem markieren und viele Barrieren abbauen, die der breiteren Einführung von async bisher im Weg stehen.
9 Kommentare
Der GIL wirkt hier irgendwie etwas unvermittelt erwähnt … Aber selbst wenn der GIL entfernt würde,
wäre es dann nicht besser, statt Python eine andere Alternative zu wählen,
wenn man sowohl bei I/O-bound- als auch bei CPU-bound-Workloads Multithreading nutzen möchte …
Bei
asynciohabe ich auch den Eindruck, dass die Abneigung unter Leuten, die tief in Python drinstecken, ziemlich stark ist.Ich habe öfter die Meinung gehört, dass eigentlich
geventzum Mainstream hätte werden sollen.Ich stimme zu, dass man kaum erwarten kann, dass die aktuelle Richtung beim GIL im Vergleich zu „anderen Alternativen“ in nichts nachsteht.
Aber wenn man sagt, man müsse statt Python andere Alternativen wählen, sollte das dann nicht eher zu der Argumentation führen, dass es ein Problem gibt, statt zu der Argumentation, dass es kein Problem gibt?
asynciowird viel genutzt … es ist durchaus brauchbar. Es gibt zwar die Einschränkung, dass das Abbrechen von Tasks edge-triggered erfolgt (und nicht level-triggered), aber in der Praxis schreibt man ohnehin eher selten Code, der sich des Task-Abbruchs bewusst ist und ihn graceful behandelt. Das größere Problem ist eher, dass der Event Loop nur eine Weak Reference auf Tasks hält und diese daher durch den GC verschwinden können … das lässt sich aber mit Structured Concurrency lösen.Für die meisten wichtigen I/O-bezogenen Aufgaben ist es kein Problem, Bibliotheken mit
asyncio-Support zu finden …Mit dem GIL hat das nicht besonders viel zu tun … der Ansatz,
asynciofür parallele CPU-intensive Aufgaben einzusetzen, wirkt an sich schon etwas seltsam. Wenn der GIL verbessert wird, dürfte das für CPU-intensive Multithreading-Workloads nützlich sein … Async dient dazu, I/O-Engpässe möglichst effizient auszunutzen …Wie auch immer, das Fazit … es gibt zwar einige Designprobleme, aber beim Erreichen des Ziels gibt es in der Praxis keine besonderen Schwierigkeiten, und wir setzen es in Production gut ein.
Haben Sie schon einmal erlebt, dass Tasks vom GC eingesammelt werden?
Natürlich nutze ich in der Produktion ebenfalls bis zum Überdruss
.asyncio, aber mit der aktuellen Nutzungserfahrung bin ich nicht so zufrieden, dass ich sagen würde: „Ich nutze es gut.“Das heutige
asyncioist gewissermaßen eine Ausweichstrategie für den GIL, da es unter der Voraussetzung des GIL entworfen wurde; insofern interagiert der GIL nicht direkt mitasyncio.Betrachtet man jedoch die gesamte auf
asynciobasierende Concurrency-Programmierung, dann ist die Aussage, der GIL sei irrelevant, meiner Meinung nach so etwas wie: „Weil es Python ist, ist es selbstverständlich, dass es nicht geht.“Ich bleibe einfach bei joblib.
Das Problem von Asyncio ist nicht der Schwierigkeitsgrad schwieriger asynchroner Programmierung, sondern die miserable Qualität. Ein Design, das Konsistenz und Allgemeingültigkeit über Bord wirft, ist in Python zwar nicht einmal selten, aber bei Dingen wie
ProactorEventLoopgibt es immer noch Bugs, die schon vor fünf Jahren gemeldet wurden und bis heute Service-Ausfälle verursachen.Wenn man gezwungen ist, es zu benutzen, fällt es einem wirklich schwer, über solche Artikel einfach hinwegzulachen.
Natürlich kann ein wichtigerer Grund sein, dass der Nutzen, den man überhaupt erzielen kann, wegen des GIL im Vergleich zu anderen Umgebungen geringer ist.
Ich halte die Formulierung, ohne GIL könne man Synergien erzielen, für nahezu irreführend. Wenn man einem Läufer, dem ein Bein fehlt, eine zwar unbequeme, aber immerhin funktionale Prothese gibt – ist das dann wirklich eine „Synergie“?