,Stefan Schwarzer schrieb am 20.01.2018 um 00:02:
On 2018-01-19 11:17, Stefan Behnel wrote:
Sven R. Kunze schrieb am 18.01.2018 um 20:02:
On 17.01.2018 22:49, Stefan Behnel wrote: Das Ineinander-Stöpseln, was ich da erlebt habe, war dann mit so einer riesigen Menge Boilerplate verbunden, dass ich diese Implementierung nicht wirklich ernstnehmen konnte. Für mich war es eher nur Show-Case anstelle einer Implementierung mit Mehrwert. Da wäre ich jetzt wirklich interessiert an realem Beispielen.
Um beim Beispiel zu bleiben, SQLAlchemy lässt sich mit ca. 10-20 Zeilen leicht lesbarem Code mit Hilfe eines Thread-Pools in async-Frameworks integrieren. Dann rufst du statt "query.all()" eben "await background_query(query.all)" auf, und schon blockiert's nicht mehr und erlaubt gleichzeitig 'unbegrenzten' I/O-Durchsatz. Empfinde ich jetzt noch nicht als "riesige Menge Boilerplate", und funktioniert auch mit etlichen anderen thread-sicheren synchronen APIs.
Mich würde es sehr interessieren, diesen "leicht lesbaren" Code zu sehen. :-) Ist der auch leicht lesbar für jemanden, der mit asyncio noch relativ wenig Erfahrungen hat?
Wie schwierig ist es unter diesen Umständen, selber auf diesen Code zu kommen?
Absolut, hier ist die triviale Minimalversion: """ size_of_connection_pool = config.get(...) pool = ThreadPoolExecutor(max_workers=size_of_connection_pool) def background_query(func, *args, **kwargs): return pool.submit(func, *args, **kwargs) """ Der Produktivcode ist dann doch noch ein bisschen komplexer, eben eher so 20 Zeilen, aber da ist schon wieder einiges Projektspezifische drin. Eine eigene Funktion (statt direkt den Pool) dafür zu nehmen hat den Vorteil, dass es a) die komplette Funktionalität hinter einem hübschen Namen kapselt, und b) der explizite Funktionsname es den Entwicklern schwerer macht, zu sagen: "och, da ist eh schon ein Thread-Pool, dann kann ich den ja auch noch für ein paar andere Dinge nehmen". Genau das sollte nämlich nicht getan werden, um das Ganze kontrollierbar zu halten. Übrigens ist die DB-Anbindung hier der optimale Fall, weil der Connection-Pool ja ohnehin eine feste Maximalgröße hat. Da können dann nochmal genau so viele Threads drauf warten, und fertig.
Ein weiterer Punkt: Ist das generischer Code, der sich auch auf andere synchrone APIs anwenden lässt oder ist es überwiegend Code, der speziell auf SQLAlchemy zugeschnitten ist?
Das ist genau der Grund, weshalb ich sowas nicht in eine Bibliothek stecken würde, sondern entweder direkt im Projekt oder in einem Commons-Modul habe. Oft lohnt es sich, die Funktionalität in der Wrapperfunktion ein bisschen auszuweiten. Findet sich eigentlich immer was, sei es spezifisches Logging, Fehlerbehandlungen, oder was auch immer. Dann wird die "background_query" Funktion eben selbst zu einer async-def Koroutine und loggt nach dem Aufruf z.B. noch die Ausführungsdauer raus.
Ein paar Gedanken zur Vermittlung von Asyncio in Python:
Die meisten Artikel, die ich dazu bisher gesehen habe, sind Einführungen anhand relativ einfacher Beispiele. Um den Leser nicht gleich zu "erschrecken", werden Aufgabe und Lösung so gewählt, dass sie gut ins Asyncio-Paradigma bzw. den Support dafür in Python passen.
Aber wie geht es weiter, wenn man sich nicht nur einen Überblick verschaffen, sondern Pythons Asyncio in "richtigen" Projekten einsetzen will? Da habe ich bisher extrem wenig Dokumentation zu gefunden. Ich glaube, das hier geht in die richtige Richtung: https://pymotw.com/3/asyncio/index.html .
Na ja, in den seltensten Fällen wirst du asyncio direkt verwenden. Für alles Web-basierte (Webseiten oder REST-APIS) gibt es z.B. Tornado, aiohttp oder Twisted-Web als Frameworks (Tornado und Twisted sind übrigens auch schon deutlich älter als asyncio, können es aber inzwischen verwenden).
Auf der anderen "Seite" von Einsteiger-Tutorials finden sich die technisch ausführliche Dokumentation wie die unter https://docs.python.org/3/library/asyncio.html . Da mag zwar alles drin stehen, aber als Einsteiger sieht man den Wald vor lauter Bäumen nicht. Das ist, wie eine natürliche Sprache aus einem Wörterbuch und einer Grammatik-Referenz lernen zu wollen.
Das Gute ist: die Doku kannst du als Normalnutzer fast komplett ignorieren. Betrachte asyncio am besten als Infrastrukturbaustein. So, wie es mit dem ThreadPoolExecutor eine hübsche API für das threading-Modul gibt, gibt es auch entsprechende Frameworks und Tools, die auf asyncio aufsetzen. Vor allem Twisted ist ja ein großer Baukasten mit allen möglichen Netzwerkprotokollen, bis hin zu SMTP und SSH, und Tornado und aiohttp sind schicke Web-Frameworks. Das Gute an asyncio und async/await gegenüber der Zeit davor, ist, dass all diese Frameworks nun kompatibel werden, und viel async-Code sich über Framework-Grenzen hinweg wiederverwenden lässt. Übrigens ganz ähnlich wie bei Generatoren in Python. Damit lässt sich auch vieles generisch und wiederverwendbar implementieren.
Stefan, ich hatte auch schon überlegt, dir vorzuschlagen, dazu einen Vortrag auf der PyCon DE zu halten. Ich fürchte aber, dass ein Vortrag nicht das richtige Medium dafür ist bzw. in der verfügbaren Zeit kaum über eine Asyncio-Einführung hinauskommt. Was meinst du?
Na ja, das Problem ist so ein bisschen: es gibt da ja eigentlich nur wenig Neues. Ja, es funktioniert jetzt so langsam alles mit allem, aber das, was da funktioniert, das funktioniert eigentlich auch schon länger. Wo also ist da das interessante Thema für einen Vortag, der den ZuhörerInnen inhaltlich auch irgendwas bringt? Asyncio ist da halt die falsche Ebene, und zwar die völlig falsche. Die meisten async-Anwendungen, an denen ich gearbeitet habe, verwenden Tornado, ein paar statt dessen Twisted oder aiohttp. Dazu verschiedene Datenbanken und andere Backends, externe Services, Queues oder was weiß ich was. Der I/O-Loop selbst ist auf der Ebene völlig irrelevant, der muss nur einfach da sein und funktionieren. Und der Anwendungscode sieht dann auch nicht wesentlich anders aus als anderswo auch, nur eben mit async/await oder Py2-Koroutinen statt "normalen" Funktionen. Das ist ja gerade der Vorteil von async/await, dass sich eigentlich nichts an der Programmierung ändert. Nur Kommunikation und Delegation wird eben mit "await" (oder "yield" in Py2) explizit sichtbar im Code. Und die komplette Anwendung läuft weiterhin single-threaded, vermeidet also Race-Conditions und die ganzen Multi-Threading-Fußfallen, die auftreten, sobald ich (aus Effizienzgründen) irgendwelchen Zustand global halten muss. Stefan