Zurück zum Katalog
Season 10 20 Episoden 1h 19m 2026

asyncio

v3.14 — Edition 2026. Ein tiefer Einblick in Pythons asyncio Framework, der den Event Loop, Coroutines, Structured Concurrency, Synchronization Primitives und fortgeschrittene asynchrone Muster abdeckt. Für Python 3.14.

Python Core Asynchrone Programmierung
asyncio
Aktuelle Wiedergabe
Click play to start
0:00
0:00
1
Der Event Loop & das mentale Modell
Baue dein grundlegendes mentales Modell für asyncio auf. Lerne, wie der Event Loop wie ein Orchesterdirigent agiert und Aufgaben kooperativ verwaltet, ohne sich auf Multithreading zu verlassen.
4m 28s
2
Coroutines vs. Awaitables
Entmystifiziere die Schlüsselwörter async und await. Wir untersuchen die entscheidende Unterscheidung zwischen einer Coroutine-Funktion und einem Coroutine-Objekt und was tatsächlich passiert, wenn du eine Operation mit await aufrufst.
3m 40s
3
Der asyncio.run() Einstiegspunkt
Entdecke, wie man eine asyncio Anwendung sicher hochfährt. Wir diskutieren asyncio.run, Executor-Shutdowns und den Runner Context Manager für komplexe Loop-Lebenszyklen.
4m 04s
4
Scheduling mit Tasks
Lerne, wie man Operationen mit asyncio.create_task() nebenläufig ausführt. Wir decken die schwerwiegenden Folgen der Garbage Collection bei nicht referenzierten Tasks auf.
3m 37s
5
Structured Concurrency mit TaskGroups
Meistere Structured Concurrency. Verstehe, wie asyncio.TaskGroup mehrere nebenläufige Operationen sicher verwaltet und bei auftretenden Exceptions für saubere Teardowns sorgt.
3m 38s
6
Task Cancellation & Timeouts
Erkunde die Mechanismen zum Abbrechen von Operationen. Lerne, warum ein asyncio.CancelledError ausgelöst wird, wie man ihn in einem finally-Block behandelt und warum du ihn niemals unterdrücken solltest.
3m 50s
7
Kontrolle abgeben mit Sleep
Verstehe den wahren Zweck von asyncio.sleep(0). Entdecke, wie das Abgeben der Kontrolle verhindert, dass CPU-lastige Schleifen den Event Loop aushungern und die Anwendung einfrieren.
3m 55s
8
Synchronisation: Locks & Mutexes
Verhindere Race Conditions in async Code. Wir erkunden asyncio.Lock, diskutieren seine nicht-Thread-sichere Natur und zeigen, warum Threading-Locks deinen Event Loop einfrieren werden.
4m 07s
9
Zustandskoordination mit Events
Lerne, Signale an mehrere wartende Tasks zu senden. Wir erklären, wie asyncio.Event und asyncio.Condition ineffiziente Polling-Schleifen elegant ersetzen.
4m 16s
10
Nebenläufigkeit begrenzen mit Semaphores
Schütze anfällige Ressourcen und verhindere Sperren durch Rate-Limiting. Entdecke, wie asyncio.Semaphore die nebenläufige Ausführung begrenzt, ohne deine Architektur zu blockieren.
4m 21s
11
Producer-Consumer Workflows
Entkopple schnelle Producer sicher von langsamen Consumern. Erkunde asyncio.Queue, die Signalisierung von Task-Abschlüssen und die neuen Shutdown-Mechanismen für Queues.
3m 58s
12
High-Level Networking mit Streams
Tauche ein in High-Level IO Streams. Wir diskutieren StreamReader, StreamWriter und warum das Weglassen von await writer.drain() den Arbeitsspeicher deines Servers stillschweigend zerstören kann.
4m 12s
13
Async Server bauen
Konstruiere hochgradig nebenläufige Netzwerkserver. Lerne, wie asyncio.start_server Client-Verbindungen abstrahiert und für jeden Peer einen isolierten Task erzeugt.
4m 26s
14
Nicht-blockierende Subprocesses
Führe Shell-Befehle asynchron aus. Entdecke, warum die Verwendung des Standard-subprocess-Moduls den Event Loop anhält und wie asyncio.create_subprocess_exec dies behebt.
3m 39s
15
Futures: Die Low-Level-Brücke
Entpacke das Fundament von await Statements. Wir untersuchen asyncio.Future, seine Rolle als zukünftiges Ergebnis und wie es alten Callback-Code mit moderner Syntax verbindet.
3m 53s
16
Transports und Protocols
Schau unter die Haube, um zu sehen, wie asyncio mit dem Betriebssystem kommuniziert. Verstehe die Callback-gesteuerte 1:1-Beziehung zwischen Transports (wie sich Bytes bewegen) und Protocols (was Bytes bedeuten).
4m 39s
17
Threading in einer Async-Welt
Verbinde synchrone und asynchrone Welten. Lerne, wie man schweren blockierenden Code mit Executors und Thread-sicheren Callbacks sicher auslagert, ohne den Loop zu blockieren.
3m 27s
18
Async Generators & Cleanup
Vermeide Ressourcenlecks mit Async Generators. Wir untersuchen, warum eine 'async for'-Iteration bei Unterbrechung hängende Verbindungen hinterlassen kann und wie aclosing() für Sicherheit sorgt.
3m 41s
19
Den Debug-Modus meistern
Finde Concurrency-Bugs sofort. Lerne, wie man PYTHONASYNCIODEBUG nutzt, um langsame Callbacks zu profilen, nicht mit await aufgerufene Coroutines aufzudecken und nie abgerufene Exceptions genau zu lokalisieren.
3m 51s
20
Erweitern & Custom Loops
Das Finale. Wir erkunden fortgeschrittene Integration und was nötig ist, um einen Custom Event Loop zu schreiben oder BaseEventLoop für spezialisierte, hochperformante Umgebungen abzuleiten.
4m 09s

Episoden

1

Der Event Loop & das mentale Modell

4m 28s

Baue dein grundlegendes mentales Modell für asyncio auf. Lerne, wie der Event Loop wie ein Orchesterdirigent agiert und Aufgaben kooperativ verwaltet, ohne sich auf Multithreading zu verlassen.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 1 von 20. Viele Entwickler hören das Wort asynchron und gehen davon aus, dass ihr Code parallel auf mehreren CPU-Kernen ausgeführt wird. Doch dann schauen sie sich ihre Anwendung an und stellen fest, dass sie komplett auf einem einzigen Thread läuft. Das Geheimnis dieser Effizienz ohne echte Parallelität ist der Event Loop, und sein mentales Modell zu verstehen, ist die Grundlage von asyncio. Der Event Loop ist der zentrale Execution Manager jeder asyncio-Anwendung. Er ist genau das, was der Name schon sagt: ein kontinuierlicher Loop, der prüft, ob Operationen zur Ausführung bereit sind, diese ausführt und dann nach der nächsten Operation sucht. Es ist extrem wichtig, dieses Konzept von Multithreading zu trennen. In einem multithreaded Programm steuert das Betriebssystem die Ausführung. Das OS pausiert zwangsweise einen Thread und wechselt zu einem anderen, um die CPU-Zeit aufzuteilen. Die Threads selbst haben keine Kontrolle darüber, wann sie pausiert werden. Das erfordert erheblichen System-Overhead, um die Context Switches zu verwalten und den Shared Memory zu schützen. Der Event Loop arbeitet mit einem völlig anderen Modell, dem sogenannten Cooperative Multitasking. Alles läuft sequenziell auf einem einzigen Thread ab. Der Loop unterbricht niemals eine Operation. Stattdessen verlässt er sich darauf, dass der Code die Kontrolle explizit an den Loop zurückgibt, wenn er auf etwas warten muss. Stell dir den Event Loop wie einen erfahrenen Koch in einer geschäftigen Restaurantküche vor. Der Koch bekommt mehrere Bestellungen gleichzeitig. Wenn er einen großen Topf Brühe zum Köcheln auf den Herd stellt, bleibt er nicht davor stehen und starrt auf die Flüssigkeit, bis sie fertig ist. Dieser Ansatz würde die gesamte Küche blockieren und nichts anderes würde gekocht werden. Stattdessen macht der Koch den Herd an, lässt den Topf köcheln und widmet sich sofort dem Gemüseschneiden für ein anderes Gericht. Der Koch repräsentiert den einzigen Execution Thread. Der Event Loop ist der Koch, der ständig die Küche scannt, genau weiß, welche Töpfe köcheln, welche Pfannen gewendet werden müssen, und sofort zum nächsten verfügbaren Job wechselt. In deiner Software ist ein köchelnder Topf normalerweise eine Input- oder Output-Operation. Wenn dein Code einen Request an eine Datenbank sendet, braucht die Datenbank Zeit, um die Query zu verarbeiten und die Daten zurückzusenden. Ein traditionelles synchrones Programm würde einfrieren und auf die Response warten. Mit einem Event Loop registriert die Operation ihren Request und teilt dem Loop dann mit, dass sie wartet. Der Event Loop wechselt sofort zu einem anderen Stück Code, das tatsächlich Daten zur Verarbeitung bereit hat. Wenn die Datenbank endlich antwortet, signalisiert die ursprüngliche Operation dem Event Loop, dass sie bereit ist, weiterzumachen. Der Event Loop packt sie zurück in die Queue und wird die Ausführung fortsetzen, sobald der aktuelle Job yieldet. Hier ist die entscheidende Erkenntnis. Weil der Event Loop eine Operation nicht zwangsweise stoppen kann, ist das gesamte System komplett auf Kooperation angewiesen. Wenn ein Job beschließt, eine riesige mathematische Berechnung durchzuführen, ohne jemals die Kontrolle abzugeben, stoppt der Event Loop. Der einzige Thread ist belegt. In unserer Küche ist das der Koch, der beschließt, einen riesigen Sack Mehl von Hand zu mahlen und dabei alle anderen Gerichte ignoriert. Die köchelnden Töpfe kochen über, neue Bestellungen häufen sich an, und die Küche kommt zum Stillstand. Der Loop ist nur so effizient wie der Code, der in ihm läuft. Echte asynchrone Effizienz kommt nicht davon, dass man mehrere Berechnungen im exakt selben physischen Moment ausführt, sondern davon, dass man sicherstellt, dass dein einzelner Thread keine einzige Millisekunde im Leerlauf verschwendet, während er auf die Außenwelt wartet. Wenn du helfen willst, die Show am Laufen zu halten, kannst du uns unterstützen, indem du auf Patreon nach DevStoriesEU suchst. Danke fürs Zuhören, Happy Coding euch allen!
2

Coroutines vs. Awaitables

3m 40s

Entmystifiziere die Schlüsselwörter async und await. Wir untersuchen die entscheidende Unterscheidung zwischen einer Coroutine-Funktion und einem Coroutine-Objekt und was tatsächlich passiert, wenn du eine Operation mit await aufrufst.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 2 von 20. Du schreibst eine Funktion, du rufst die Funktion auf, und absolut nichts passiert. Dein Code läuft ohne Fehler, aber die Datenbank ist leer und der Network Request wird nie ausgeführt. Das Problem ist ein grundlegendes Missverständnis darüber, was der Aufruf einer asynchronen Funktion eigentlich macht. Heute schauen wir uns Coroutines vs Awaitables an. Im normalen Python wird eine Standardfunktion sofort ausgeführt, wenn du sie aufrufst. Asynchrone Funktionen brechen diese Regel komplett. Es gibt einen strikten Unterschied zwischen einer Coroutine Function und einem Coroutine Object. Wenn du async def schreibst, erstellst du eine Coroutine Function. Wenn du diese Funktion in deinem Code aufrufst, wird der Body der Funktion nicht ausgeführt. Stattdessen gibt sie ein Coroutine Object zurück. Stell dir das wie eine Kaffeebestellung vor. Die async def Funktion ist der Menüpunkt. Diese Funktion aufzurufen ist, als würdest du deine Bestellung an der Kasse aufgeben. Du bekommst einen Beleg. Dieser Beleg ist dein Coroutine Object. Du hast deine Absicht erklärt, aber du hast dein Getränk noch nicht, und niemand hat überhaupt angefangen, es zuzubereiten. Um den Brühvorgang wirklich zu starten und deinen Kaffee zu bekommen, musst du an der Theke warten. In Python machst du das mit dem await Keyword. Wenn du await tippst, gefolgt von diesem Coroutine Object, passieren zwei verschiedene Dinge. Erstens fängt die Coroutine endlich an, ihren internen Code auszuführen. Zweitens pausiert die Funktion, in der du das await platziert hast, komplett. Sie gibt die Kontrolle an Python zurück und sagt, dass sie nicht weitermachen kann, bis diese spezifische Coroutine fertig ist. Dieses Pausieren ist der zentrale mechanische Unterschied der asynchronen Programmierung. Während deine Funktion pausiert und auf den Kaffee wartet, kann Python problemlos woanders anderen Code ausführen. Das bringt uns zum weiter gefassten Begriff Awaitable. Ein Awaitable ist einfach jedes Objekt, das Python dir erlaubt, mit dem await Keyword zu benutzen. Alle Coroutines sind Awaitables. Wenn du await siehst, lies es als direkten Befehl: Führe dieses Awaitable Object bis zum Ende aus und pausiere meinen aktuellen Fortschritt, bis es ein Endergebnis liefert. Wenn du eine async Funktion namens fetch data schreibst, gibt der einfache Aufruf von fetch data das Coroutine Object zurück. Wenn du diesen Aufruf einer Variable namens pending request zuweist, enthält diese Variable einfach nur die nicht ausgeführte Coroutine. Das Netzwerk bleibt komplett still. Später in deinem Skript, wenn du await pending request schreibst, führt Python den Network Call endlich aus. Die Ausführung deines aktuellen Code Blocks stoppt genau an dieser Zeile. Sobald der Server antwortet, löst sich die await Expression in die zurückgegebenen Daten auf, und dein umgebender Code macht in der nächsten Zeile weiter. Hier ist der entscheidende Punkt. Du kannst das await Keyword nur innerhalb einer async def Funktion verwenden. Weil das Awaiten eines Objects erfordert, dass die aktuelle Ausführung pausiert wird, muss die Funktion, die das await enthält, selbst pausiert werden können. Deshalb breitet sich asynchrones Verhalten nach außen aus. Um eine Coroutine zu awaiten, musst du dich innerhalb einer Coroutine befinden. Du baust eine Chain von pausierten Operationen auf, die alle darauf warten, dass der Task auf der untersten Ebene resolved wird. Denk dran: Eine async Funktion aufzurufen, ohne sie zu awaiten, erzeugt nur einen Beleg für Arbeit, die du eigentlich nie in Auftrag gegeben hast. Der Code wird niemals laufen, bis du ihn awaitest. Danke fürs Einschalten. Bis zum nächsten Mal!
3

Der asyncio.run() Einstiegspunkt

4m 04s

Entdecke, wie man eine asyncio Anwendung sicher hochfährt. Wir diskutieren asyncio.run, Executor-Shutdowns und den Runner Context Manager für komplexe Loop-Lebenszyklen.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 3 von 20. Wenn du den Startpunkt deiner asynchronen Anwendung falsch nutzt, können dangling Thread Executors und nicht geschlossene Async Generators zurückbleiben. Um versteckte Resource Leaks zu vermeiden, brauchst du das richtige Tool zum Starten und Stoppen deiner Anwendung. Und das bringt uns zum asyncio.run Entry Point. Viele Entwickler versuchen fälschlicherweise, mit diesem Tool einzelne Coroutines willkürlich aus synchronem Code heraus auszuführen. Dafür ist es nicht gedacht. Du kannst die run Funktion nicht aufrufen, wenn bereits ein anderer asyncio Event Loop im exakt selben Thread läuft. Wenn du das tust, wirft das sofort einen Runtime Error. Sie ist speziell dafür gedacht, der einzige High-Level Entry Point für ein Programm zu sein. Stell dir vor, du initialisierst den Main Loop eines Webservers, der alle eingehenden Traffic Requests koordiniert. Du hast eine zentrale asynchrone Funktion, die sich an einen Network Port bindet, die Request Handler einrichtet und den Server am Laufen hält. Genau diese eine Main Funktion übergibst du an die run Funktion. Wenn du das machst, managt asyncio den kompletten Lifecycle des Event Loops automatisch. Zuerst erstellt es einen neuen Event Loop und setzt ihn als den aktuell aktiven Loop für den Thread. Als Nächstes führt es deine Main Webserver Coroutine aus, bis sie abgeschlossen ist. Hier ist der entscheidende Punkt. Die wichtigste Arbeit dieser Funktion passiert, nachdem dein Main Code fertig ausgeführt wurde. Sie führt ein gründliches Cleanup durch. Bevor die Kontrolle an den synchronen Teil deines Programms zurückgeht, bricht sie alle verbleibenden Pending Tasks ab. Danach fährt sie Background Threads im Default Executor sicher herunter. Zum Schluss finalisiert sie alle Async Generators, bevor der Event Loop komplett geschlossen wird. Du kannst dieser Funktion auch ein Debug Flag übergeben, was den zugrunde liegenden Loop zwingt, im Debug Mode zu laufen, um Execution Issues aufzuspüren. Weil diese Standardfunktion am Ende alles abbaut, schafft sie eine strikte Grenze. Wenn du ein Szenario hast, in dem du mehrere separate asynchrone Blöcke aus synchronem Code heraus ausführen musst, sie sich aber denselben Event Loop teilen sollen, schlägt der wiederholte Aufruf der Standard run Funktion fehl. Denn jedes einzelne Mal wird ein neuer Loop erstellt und wieder zerstört. Für genau diese Situation nutzt du den asyncio Runner Context Manager. Du öffnest einen Context Block mit dem Standard Python with Statement. Beim Betreten dieses Blocks wird der Event Loop initialisiert. Sobald du drin bist, kannst du die eigene run Methode des Runner Objekts aufrufen. Du übergibst ihr eine Coroutine, sie führt sie bis zum Ende aus und gibt das Ergebnis zurück. Du kannst diese interne run Methode innerhalb desselben Context Blocks mehrmals aufrufen. Der Event Loop bleibt aktiv und behält State, Cached Data und Connections zwischen diesen separaten Aufrufen bei. Du kannst den Context Manager bei der Erstellung konfigurieren, indem du ein Debug Flag oder sogar eine Custom Loop Factory übergibst, falls deine Umgebung eine spezielle Event Loop Implementierung erfordert. Wenn die Execution den Context Manager Block schließlich verlässt, führt der Runner die exakt gleiche Teardown Sequence aus wie die Standalone Funktion. Er räumt die Executors auf, finalisiert die Generators und schließt den Loop sicher. Die Stabilität deiner Anwendung hängt komplett davon ab, wie sie startet und endet. Ob du einen einzelnen Function Call oder den Context Manager nutzt: Deine Execution über diese offiziellen Entry Points zu leiten, ist der einzige Weg, um zu garantieren, dass deine asynchronen Ressourcen beim Beenden des Programms zuverlässig abgebaut werden. Das war's für diese Folge. Danke fürs Zuhören und keep building!
4

Scheduling mit Tasks

3m 37s

Lerne, wie man Operationen mit asyncio.create_task() nebenläufig ausführt. Wir decken die schwerwiegenden Folgen der Garbage Collection bei nicht referenzierten Tasks auf.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 4 von 20. Du startest einen Background Process, um System Metrics zu verschicken. Später checkst du dein Dashboard und stellst fest, dass die Hälfte der Daten fehlt. Es wurden keine Errors geworfen. Dein Code hat mitten in der Execution einfach stillschweigend gestoppt. Das passiert, weil du deinen Background Job als Fire-and-Forget behandelt hast. Heute geht es um Scheduling mit Tasks, und warum du die Dinge, die du erstellst, immer behalten musst. Wenn du eine Coroutine hast, die du concurrent zu anderem Code ausführen willst, nutzt du die asyncio create task Funktion. Du übergibst deine Coroutine an diese Funktion, und asyncio verpackt sie in ein Task-Objekt. Das sagt dem Event Loop, dass er den Task für die Execution schedulen soll. Die Funktion gibt dir das neue Task-Objekt sofort zurück, sodass dein Main-Programm weiterlaufen kann, während der Task im Background arbeitet. Viele Developer rufen create task auf und ignorieren den Return Value. Das ist eine riesige Falle. Hier ist die entscheidende Erkenntnis. Der asyncio Event Loop hält nur Weak References auf die Tasks, die er ausführt. Der Loop selbst schützt deinen Task nicht vor Pythons Garbage Collector. Wenn du das zurückgegebene Task-Objekt keiner Variable zuweist oder in einer Datenstruktur speicherst, merkt der Garbage Collector irgendwann, dass keine Hard References existieren. Wenn das passiert, zerstört Python das Task-Objekt. Es ist ihm völlig egal, ob die Coroutine gerade mitten in einer Database Query steckt oder auf eine Network Response wartet. Der Task verschwindet einfach. Denk an eine async Funktion namens ship metrics. Sie formatiert einen Data Payload und sendet einen HTTP Request an einen externen Server. Du rufst create task auf und übergibst ship metrics, aber du weist das Result keiner Variable zu. Der Task fängt an zu laufen. Er formatiert den Payload. Dann kommt er zum Network Call und pausiert, um auf eine Connection zu warten. Während er pausiert ist, läuft der Garbage Collector. Der Strong Reference Count ist null. Der Task wird zerstört. Der Server empfängt den Payload nie, und deine Application loggt keinen Error, weil die Execution einfach aufgehört hat zu existieren. Um das zu verhindern, musst du immer eine Strong Reference auf die Tasks behalten, die du schedulest. Wenn du einen einzelnen Task erstellst, weise ihn einer Variable zu. Wenn du mehrere Background Tasks in einem Loop schedulest, füge sie einem Standard-Python-Set oder einer List hinzu. Solange dieses Set im Memory existiert, bleiben die Strong References bestehen, und der Garbage Collector lässt deine laufenden Tasks in Ruhe. Du kannst dann einen Callback nutzen, um den Task aus deinem Set zu entfernen, sobald er fertig ist. Die create task Funktion akzeptiert außerdem ein paar optionale Arguments. Du kannst einen String an den name Parameter übergeben, was dem Task einen spezifischen Identifier zuweist. Das wird fürs Debugging extrem empfohlen, da es viel einfacher macht, herauszufinden, welche spezifische Operation fehlgeschlagen ist, falls später eine Exception geworfen wird. Du kannst auch ein context Argument übergeben, um einen bestimmten Context Variable State für den Task festzulegen. Background Operations als Fire-and-Forget zu behandeln, wird sich früher oder später durch Silent Failures rächen. Wenn du den Event Loop anweist, etwas auszuführen, musst du eine Hard Reference auf das resultierende Objekt behalten, bis die Arbeit komplett abgeschlossen ist. Danke fürs Zuhören, Happy Coding zusammen!
5

Structured Concurrency mit TaskGroups

3m 38s

Meistere Structured Concurrency. Verstehe, wie asyncio.TaskGroup mehrere nebenläufige Operationen sicher verwaltet und bei auftretenden Exceptions für saubere Teardowns sorgt.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 5 von 20. Vor Python 3.11 war es zwar einfach, mehrere Tasks gleichzeitig zu starten, aber sie sicher zu handhaben, wenn einer gecrasht ist, war bekanntermaßen schwierig. Oft hattest du am Ende verwaiste Background Tasks, die still und heimlich Ressourcen verschwendet haben. Die Lösung für dieses Chaos ist Structured Concurrency mit einer TaskGroup. Eine TaskGroup ist ein asynchroner Context Manager. Man verwechselt sie manchmal mit einer normalen Liste von Tasks, aber sie ist viel strenger. Sie bietet starke Sicherheitsgarantien dafür, wie Tasks beginnen und enden. Sie erzwingt die Regel, dass eine Parent Routine erst dann beendet werden kann, wenn alle ihre Child Operations entweder abgeschlossen oder sauber gecancelt wurden. Du nutzt sie, indem du einen async with-Block öffnest. Innerhalb dieses Blocks rufst du die create task-Methode direkt auf dem Group-Objekt auf, um deine concurrent Operations zu starten. Du wirfst diese Tasks nicht in ein Standard-Array und awaitest sie manuell. Stattdessen pausiert die TaskGroup implizit, sobald der Code das Ende des async with-Blocks erreicht. Sie wartet genau dort, bis jeder gespawnte Task abgeschlossen ist. Der Block wird einfach nicht vorzeitig verlassen. Hier ist der entscheidende Punkt. Die wahre Stärke einer TaskGroup liegt darin, wie sie mit Fehlern umgeht. Bei Legacy-Tools wie gather liefen die anderen Tasks im Hintergrund weiter, wenn du mehrere Tasks gestartet hast und einer einen Error geworfen hat. Du musstest komplexe Error-Handling-Logik schreiben, um die Überlebenden aufzuspüren und zu killen. Eine TaskGroup erledigt das automatisch. Nehmen wir das Szenario eines Web Scrapers, der gleichzeitig drei verschiedene API Endpoints abruft. Du brauchst User-Daten, aktuelle Posts und System-Alerts. Du öffnest eine TaskGroup und spawnst drei Tasks. Sie laufen alle concurrent über das Netzwerk. Mitten in der Operation bekommt der Endpoint für aktuelle Posts einen Timeout und raist einen Connection Error. Die TaskGroup erkennt diesen Fehler sofort. Sie fängt den Error ab und sendet automatisch ein Cancellation-Signal an den User-Daten-Task und den System-Alerts-Task. Sie räumt diese pending Operations auf, damit sie nicht weiter Netzwerkbandbreite oder Memory fressen. Die verbleibenden Tasks raisen intern einen Cancelled Error und bestätigen so den Shutdown. Sobald alle verbleibenden Tasks sicher gestoppt sind, bündelt die TaskGroup den ursprünglichen Connection Error in einer neuen Struktur namens ExceptionGroup und raist sie aus dem Context-Block heraus. Dieses Verhalten macht deinen asynchronen Code komplett vorhersehbar. Wenn die Execution den Block erfolgreich passiert, weißt du sicher, dass jeder einzelne Task erfolgreich war. Wenn der Block eine ExceptionGroup raist, weißt du, dass der Fehler abgefangen und alles andere ordnungsgemäß heruntergefahren wurde. Du lässt niemals Rogue Tasks im Hintergrund laufen. Wenn du die Ergebnisse der erfolgreichen Tasks brauchst, kannst du sie direkt aus den erstellten Task-Objekten abrufen, vorausgesetzt, sie wurden abgeschlossen, bevor der Fehler auftrat. Indem sie Tasks an einen strikten Lifecycle-Block binden, garantieren TaskGroups, dass concurrent Operations deine Anwendung als eine einzige, koordinierte Einheit betreten und verlassen. Das war’s für heute. Danke fürs Zuhören – leg los und bau etwas Cooles.
6

Task Cancellation & Timeouts

3m 50s

Erkunde die Mechanismen zum Abbrechen von Operationen. Lerne, warum ein asyncio.CancelledError ausgelöst wird, wie man ihn in einem finally-Block behandelt und warum du ihn niemals unterdrücken solltest.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 6 von 20. Du hast einen robusten Error Handler geschrieben, der alle generischen Exceptions in deinem Async Worker abfängt. Doch jetzt, beim Shutdown, verstopft dein Event Loop mit Zombie-Tasks, die einfach nicht sterben wollen. Dein Sicherheitsnetz hält sie quasi lebendig gefangen. Genau darum geht es heute: Task Cancellation und Timeouts. Wenn du einen laufenden Task stoppen musst, rufst du seine cancel-Methode auf. Das beendet den Task nicht sofort, wie wenn man einen Systemprozess killt. Stattdessen fordert asyncio einen Stopp an, indem ein Error, genauer gesagt ein asyncio CancelledError, in den Task injiziert wird. Dieser Error wird genau am aktuellen oder nächsten await des Tasks ausgelöst. Die Coroutine wickelt dann ihren Stack ab, genau wie bei jedem Standard-Python-Error. Dieser Mechanismus ist auch die Grundlage für Timeouts. Wenn du einen Task in eine Timeout-Funktion wrappst und der Timer abläuft, stoppt der Event Loop den Task nicht auf magische Weise. Er ruft einfach cancel auf diesem Task auf. Der Task empfängt beim nächsten await den CancelledError, wickelt seinen State ab und meldet schließlich dem Timeout-Wrapper, dass er gestoppt hat. Erst dann wirft der Timeout-Wrapper einen TimeoutError zurück an dich. Hier ist die entscheidende Erkenntnis. Seit Python 3.8 erbt CancelledError direkt von BaseException und nicht von der Standard-Exception-Klasse. Diese Designentscheidung verhindert einen bestimmten, katastrophalen Fehler. Entwickler wrappen Netzwerk- oder Dateioperationen routinemäßig in try- und except-Blöcke, die generische Exception-Klassen abfangen, um einen Crash zu verhindern. Wäre CancelledError eine Standard Exception, würden diese Blöcke das Cancellation-Signal abfangen. Der Task würde vielleicht eine Warnung loggen, das Signal schlucken und als Zombie weiterlaufen. Indem CancelledError in der Hierarchie nach oben zu BaseException verschoben wurde, garantiert Python, dass deine alltäglichen Error Handler nicht versehentlich einen Cancellation-Request abfangen. Wie managst du also sicher den State, wenn ein Task gecancelt wird? Du verlässt dich auf die try- und finally-Struktur. Stell dir einen Webserver vor, der einen eingehenden HTTP-Request verarbeitet. Der User fragt einen riesigen Report an, schließt dann aber sein Browserfenster. Der Server erkennt den Disconnect und cancelt den Request-Task. In deinem Code awaitest du gerade eine langlaufende Database Query. Dieses await wirft plötzlich einen CancelledError. Weil du deine Datenbankinteraktion in einen try-Block gepackt hast, springt die Ausführung sofort in deinen finally-Block. Du nutzt diesen finally-Block, um ein sauberes Rollback der ausstehenden Transaction durchzuführen und die Database Connection an den Pool zurückzugeben. Sobald der finally-Block durch ist, bubbelt der CancelledError weiter nach oben und beendet den Task erfolgreich. Manchmal reicht ein finally-Block nicht aus. Wenn du unbedingt einen asynchronen Cleanup durchführen musst, wie zum Beispiel einen Network Request an einen Remote Microservice zu senden, um den Abbruch anzukündigen, kannst du den CancelledError explizit catchen. Aber wenn du das tust, musst du genau diesen Error am Ende deines except-Blocks explizit re-raisen. Wenn du ihn nicht re-raisest, machst du die internen Mechaniken von asyncio kaputt. Der Task sieht dann so aus, als wäre er erfolgreich beendet worden, anstatt gecancelt zu werden, was den State deiner Application korrumpiert und die Structured Concurrency bricht. Die Regel, die du dir merken musst, ist, dass Cancellation ein kooperativer Request ist, kein erzwungener Kill-Command, und sich komplett darauf verlässt, dass Exceptions unberührt nach oben bubbeln. Wenn du die Show unterstützen möchtest, kannst du auf Patreon nach DevStoriesEU suchen. Das war's für diese Folge. Danke fürs Zuhören und viel Spaß beim Entwickeln!
7

Kontrolle abgeben mit Sleep

3m 55s

Verstehe den wahren Zweck von asyncio.sleep(0). Entdecke, wie das Abgeben der Kontrolle verhindert, dass CPU-lastige Schleifen den Event Loop aushungern und die Anwendung einfrieren.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 7 von 20. Manchmal liegt das Geheimnis für einen responsiven Netzwerk-Server darin, deine schwersten Tasks für exakt null Sekunden schlafen zu schicken. Wenn eine Funktion nie pausiert, reagiert deine gesamte Application nicht mehr auf die Außenwelt. Um das zu fixen, nutzt du Yielding Control mit Sleep. Im asyncio Framework führt der Event Loop immer genau einen Task gleichzeitig aus. Er verlässt sich komplett auf kooperatives Multitasking. Ein Task läuft kontinuierlich, bis er auf ein await Keyword trifft, das als Checkpoint dient, um die Execution Control an den Loop zurückzugeben. Wenn du eine async Funktion schreibst, die eine rein CPU-bound Operation enthält, erzeugst du einen Bottleneck. Denk an das Parsen eines riesigen JSON Payloads oder das Transformieren von tausenden Strings. In einem Standard Data Processing Loop gibt es keine natürlichen await Punkte. Weil der Task nie yieldet, bleibt der Event Loop blockiert. Alle eingehenden Network Requests, Database Replies oder Health Checks sitzen einfach in einer Queue und verhungern, während sie darauf warten, dass dein Loop fertig wird. Der native Weg, das zu lösen, ist, die Kontrolle manuell an den Event Loop zurückzugeben. Das machst du mit einem bestimmten Idiom: Du awaitest asyncio dot sleep mit einem Argument von null. Auf den ersten Blick sieht ein Sleep von null Sekunden wie eine nutzlose Operation aus. Warum sollte man das System bitten, für gar keine Zeit zu warten? Hier ist die entscheidende Erkenntnis. Bei einem Zero-Second Sleep geht es nicht um das Vergehen von Zeit. Es ist ein explizites Signal an den Event Loop. Wenn du einen Sleep von null awaitest, wird die aktuelle Coroutine sofort suspended. Der Event Loop übernimmt, packt deinen suspended Task ans Ende der Runnable Queue und checkt, ob andere scheduled Tasks bereit zur Ausführung sind. Wenn ein Background Network Handler darauf wartet, eine eingehende Connection zu acknowledgen, kommt er an die Reihe. Sobald die anderen Tasks ihre eigenen await Punkte erreichen oder fertig sind, arbeitet sich dein ursprünglicher Task wieder an die Spitze der Queue vor und macht genau da weiter, wo er aufgehört hat. Lass uns das auf ein konkretes Szenario anwenden. Du schreibst eine async Funktion, um Millionen von Records aus einem JSON File zu verarbeiten. Wenn du einen While Loop einfach durchlaufen lässt, wirkt dein Server wie tot. Stattdessen führst du eine Counter Variable ein. Innerhalb des Loops verarbeitest du einen Record und inkrementierst den Counter. Dann fügst du eine simple Condition hinzu. Wenn der Counter anzeigt, dass einhundert Iterationen vergangen sind, awaitest du asyncio dot sleep zero. Das bricht die massive Computation in handhabbare Chunks auf. Der Loop verarbeitet einhundert Records, tritt zur Seite, um den Server Pings beantworten oder neue Daten akzeptieren zu lassen, und macht dann mit dem Parsen der nächsten einhundert weiter. Die Anzahl der Iterationen zwischen den Yields ist ein Parameter, den du tunen musst. Bei jeder einzelnen Iteration zu yielden, erzeugt zu viel Overhead, weil das Suspendieren und Resumen einer Coroutine einen kleinen Rechenaufwand bedeutet. Alle zehntausend Iterationen zu yielden, könnte den Event Loop immer noch zu lange blockieren. Einhundert ist ein vernünftiger Startpunkt, um den Loop atmen zu lassen. Einen Zero-Second Sleep zu erzwingen, ist der einfachste Weg, deine Application kooperativ zu halten, und stellt sicher, dass ein einzelner schwerer Loop niemals den Rest deines Systems verhungern lässt. Danke fürs Zuhören, Happy Coding zusammen!
8

Synchronisation: Locks & Mutexes

4m 07s

Verhindere Race Conditions in async Code. Wir erkunden asyncio.Lock, diskutieren seine nicht-Thread-sichere Natur und zeigen, warum Threading-Locks deinen Event Loop einfrieren werden.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 8 von 20. Du packst einen Standard-Threading-Lock in deine Async-Anwendung, um eine Shared Resource zu schützen, und plötzlich friert dein gesamter Event Loop komplett ein. Der Lock hat zwar seinen Job gemacht, aber er hat alles andere im Prozess angehalten. Um das zu lösen, ohne den Loop zu blockieren, nutzen wir Synchronisation: Locks und Mutexes. Ein asyncio-Lock, oft auch Mutex genannt, garantiert den exklusiven Zugriff auf eine Shared Resource zwischen asynchronen Tasks. Zuerst müssen wir ein häufiges Missverständnis ausräumen. Du kannst keinen Standard-Thread-Lock aus dem Python-threading-Modul innerhalb einer Async-Anwendung verwenden. Ein Threading-Lock arbeitet auf Betriebssystemebene. Wenn er den Lock nicht bekommen kann, pausiert er den gesamten Thread. Da asyncio mehrere Tasks kooperativ in einem einzigen Thread ausführt, bedeutet das Blockieren dieses Threads, dass der Event Loop stoppt. Es werden keine Network Requests abgefeuert, keine Timer ticken. Alles friert ein. Ein asyncio-Lock löst dieses Problem, indem er task-safe und nicht thread-safe ist. Wenn ein asyncio-Task versucht, einen gelockten Mutex zu bekommen, blockiert er den Thread nicht. Stattdessen pausiert er sich selbst und gibt die Kontrolle an den Event Loop zurück. Dadurch können andere, unabhängige Tasks ihre Arbeit fortsetzen, während der erste Task in der Warteschlange steht. Machen wir das an einem konkreten Szenario fest. Du hast eine Anwendung mit Dutzenden von Async-Tasks, die externe API-Calls machen. Dein OAuth-Token läuft ab. Zwei verschiedene Tasks bemerken das abgelaufene Token in exakt derselben Millisekunde. Ohne Synchronisation feuern beide Tasks unabhängig voneinander einen Request an den Authentication-Server ab, um das Token zu refreshen. Diese redundante Arbeit kann Rate Limits auslösen oder das erste Token aufgrund strenger Rotation Policies sofort ungültig machen. Um diese Race Condition zu verhindern, erstellst du beim Initialisieren deiner Anwendung einen einzigen asyncio-Lock. Dieses Lock-Objekt wird an alle deine API-Tasks übergeben oder von ihnen geteilt. Schauen wir uns nun den Flow an. Task A und Task B erkennen beide das abgelaufene Token. Task A erreicht den Synchronisationsblock zuerst und wartet auf den Lock. Er bekommt ihn erfolgreich. Task B kommt den Bruchteil einer Sekunde später an und wartet auf denselben Lock. Da Task A ihn hält, legt sich Task B schlafen und lässt den Event Loop andere Aufgaben erledigen. Wenn mehrere Tasks auf denselben Lock warten, reiht asyncio sie auf. Sobald der Lock freigegeben wird, weckt der Event Loop den ersten Task in der Schlange auf. Task A fordert sicher das neue Token an, aktualisiert die geteilte Token-Variable und gibt den Lock frei. In diesem Moment weckt der Event Loop Task B auf. Task B bekommt schließlich den Lock. Bevor Task B jedoch einen Network Call macht, checkt er das Token erneut. Er sieht, dass das Token bereits gültig ist, überspringt den Refresh-Schritt, gibt den Lock frei und macht mit seinem primären API-Request weiter. Der sicherste Weg, diese Logik zu implementieren, ist die Nutzung eines asynchronen Context Managers. In deinem Code schreibst du ein async with-Statement, gefolgt vom Lock-Objekt. Wenn die Ausführung in diesen Block eintritt, wartet sie auf exklusiven Zugriff. Wenn die Ausführung den Block verlässt, entweder normal oder weil ein Fehler den Task zum Absturz gebracht hat, gibt sie den Lock automatisch frei. Du musst die acquire- oder release-Methoden nicht manuell aufrufen, was das Risiko eliminiert, einen Lock versehentlich für immer blockiert zu lassen. Hier ist die wichtigste Erkenntnis. Ein asyncio-Lock schützt deinen State nicht vor anderen Betriebssystem-Threads; er schützt deinen State davor, dass sich deine eigenen concurrent Tasks gegenseitig in die Quere kommen, während sie auf andere Operationen warten. Danke fürs Dabeisein. Ich hoffe, du hast etwas Neues gelernt.
9

Zustandskoordination mit Events

4m 16s

Lerne, Signale an mehrere wartende Tasks zu senden. Wir erklären, wie asyncio.Event und asyncio.Condition ineffiziente Polling-Schleifen elegant ersetzen.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 9 von 20. Du hast fünfzig Tasks, die auf eine Datenbankverbindung warten. Du willst auf keinen Fall, dass sie in einer Schleife pollen und CPU-Zyklen verschwenden, während sie prüfen, ob die Verbindung bereit ist. Du brauchst ein einziges Broadcast-Signal, das ihnen allen sagt, dass sie genau im selben Moment mit der Abfrage beginnen sollen. Genau das übernimmt die Zustandsverwaltung mit Events und Conditions. Ein asyncio Event verwaltet ein einfaches internes Boolean-Flag. Es startet als false. Bevor wir uns den Ablauf ansehen, klären wir eine häufige Verwechslung zwischen Events und Locks. Ein Lock gewährt genau einem Task zur gleichen Zeit exklusiven Zugriff und hält die anderen draußen. Ein Event macht genau das Gegenteil. Es benachrichtigt mehrere wartende Tasks gleichzeitig und lässt sie alle auf einmal weitermachen. Denk an dieses Datenbankverbindungs-Szenario. Dein Background-Task arbeitet daran, die Verbindung herzustellen. Währenddessen erreichen deine fünfzig Worker-Tasks einen Punkt, an dem sie die Datenbank brauchen. Jeder Worker ruft die wait-Methode auf deinem geteilten Event-Objekt auf. Weil das interne Flag false ist, werden alle fünfzig Tasks pausiert. Sie warten im Leerlauf. Irgendwann ist der Background-Task erfolgreich und ruft die set-Methode des Events auf. Das Flag wird true. Sofort wachen alle fünfzig pausierten Worker-Tasks auf und setzen ihre Ausführung fort. Wenn du die Verbindung später wieder abbauen musst, kannst du die clear-Methode auf dem Event aufrufen. Das Flag geht zurück auf false, und alle zukünftigen Aufrufe von wait blockieren wieder. Du kannst den aktuellen Status des Flags auch jederzeit überprüfen, indem du die is set-Methode aufrufst, die true oder false zurückgibt, ohne den Task zu blockieren. Das deckt einfache Broadcast-Signale ab. Manchmal reicht ein einzelnes Boolean-Flag nicht aus. Vielleicht hast du mehrere Tasks, die darauf warten müssen, dass eine geteilte Ressource einen bestimmten komplexen Zustand erreicht, und sie brauchen exklusiven Zugriff, um diesen Zustand sicher zu überprüfen oder zu ändern. Hier kommt die asyncio Condition ins Spiel. Eine Condition ist um ein zugrundeliegendes Lock herum aufgebaut. Um irgendetwas mit einer Condition zu machen, muss ein Task sie zuerst acquiren. Sobald er sie acquired hat, prüft der Task den geteilten Zustand. Wenn der Zustand nicht das ist, was der Task braucht, ruft der Task die wait-Methode der Condition auf. Hier ist die entscheidende Erkenntnis. Der Aufruf von wait auf einer Condition macht zwei Dinge gleichzeitig: Er gibt das zugrundeliegende Lock frei, wodurch andere Tasks auf den Zustand zugreifen können, und er pausiert den aktuellen Task. Während dieser Task pausiert ist, kann ein anderer Task die Condition acquiren, den geteilten Zustand ändern und dann die notify-Methode aufrufen. Die notify-Methode nimmt ein Argument, das genau angibt, wie viele wartende Tasks aufgeweckt werden sollen, standardmäßig einer. Du kannst auch notify all aufrufen, um alle auf einmal aufzuwecken. Wenn ein pausierter Task aufwacht, läuft er nicht einfach sofort los. Er muss warten, um das zugrundeliegende Lock wieder zu acquiren, bevor die wait-Methode zurückkehrt. Weil ein anderer Task sich das Lock schnappen und den Zustand ändern könnte, bevor der aufgeweckte Task an die Reihe kommt, wird der wait-Aufruf fast immer in eine while-Schleife gepackt, die den gewünschten Zustand kontinuierlich überprüft. Sobald er das Lock zurückhat und der Zustand korrekt ist, kann er sicher weitermachen und schließlich die Condition freigeben. Wenn du dich zwischen den beiden entscheidest, denk daran, dass ein Event ein einfacher Broadcast ist, der Tasks mitteilt, dass eine einmalige Aktion passiert ist, während eine Condition es Tasks ermöglicht, sicher auf eine komplexe Zustandsänderung zu warten, ohne ständig eine gelockte Ressource zu pollen. Danke, dass du ein paar Minuten mit mir verbracht hast. Bis zum nächsten Mal, mach's gut.
10

Nebenläufigkeit begrenzen mit Semaphores

4m 21s

Schütze anfällige Ressourcen und verhindere Sperren durch Rate-Limiting. Entdecke, wie asyncio.Semaphore die nebenläufige Ausführung begrenzt, ohne deine Architektur zu blockieren.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 10 von 20. Zehntausend asynchrone Requests an eine anfällige Third-Party-API abzufeuern, ist ein extrem effizienter Weg, deine IP-Adresse dauerhaft sperren zu lassen. Dein Code läuft einwandfrei, aber der Server auf der anderen Seite bricht unter dem plötzlichen Traffic-Spike zusammen. Um externe Services und deinen eigenen Zugriff zu schützen, musst du deine Application drosseln. Dieser Schutzschild ist das Limitieren der Concurrency mit Semaphores. Es hilft, gleich zu Beginn ein häufiges Missverständnis auszuräumen. Ein Semaphore ist kein Rate Limiter. Es deckelt nicht, wie viele Requests dein Programm pro Sekunde macht. Stattdessen limitiert es concurrent Operations. Es kontrolliert strikt, wie viele Tasks einen bestimmten Block von Netzwerk- oder Datei-Operationen im exakt selben Moment ausführen können. Wenn ein Task seinen API Call in zehn Millisekunden abschließt, wird dieser Slot sofort für den nächsten Task in der Warteschlange frei. Du könntest immer noch Hunderte von Operationen pro Sekunde verarbeiten, vorausgesetzt, es sind nicht mehr als das erlaubte Limit gleichzeitig in flight. Ein asyncio Semaphore verwaltet einen simplen internen Counter. Wenn du das Semaphore-Objekt erstellst, gibst du einen Initialwert an. Nehmen wir das Szenario, in dem du ausgehende HTTP Requests an eine empfindliche externe API auf exakt zehn concurrent Connections limitieren willst. Du initialisierst dein Semaphore mit einem Wert von zehn. Bevor ein asynchroner Task einen Network Request macht, muss er das Semaphore acquiren. Diese Aktion verringert den internen Counter um eins. Wenn der Network Request abgeschlossen ist, released der Task das Semaphore, wodurch der Counter wieder um eins erhöht wird. Hier liegt der entscheidende Punkt. Wenn bereits zehn Tasks das Semaphore acquired haben, steht der Counter auf null. Wenn der elfte Task versucht, es zu acquiren, wird dieser Task suspended. Die acquire-Methode blockiert den Fortschritt, bis einer der ersten zehn Tasks fertig ist und es released. Dieses simple numerische Lock stellt sicher, dass du dein hartes Limit von zehn aktiven Connections niemals überschreitest. In der Praxis solltest du die acquire- und release-Methoden nur selten manuell aufrufen. Stattdessen verwendest du das Semaphore als asynchronen Context Manager. Indem du deinen HTTP Request in ein asynchrones with-Statement packst, garantiert Python, dass das Semaphore released wird, wenn der Codeblock verlassen wird. Dieser Release passiert selbst dann, wenn die API ein Timeout hat, die Connection droppt oder eine unhandled Exception wirft. Wenn du manuelle Releases versuchst und ein Fehler deinen release-Call überspringt, ist dieser Concurrency-Slot für immer verloren. Wenn du alle zehn Slots an transiente Network Errors verlierst, gerät dein gesamtes Programm still und heimlich in einen Deadlock. Es gibt eine subtile Gefahr beim Standard-Semaphore. Wenn ein Logikfehler in deinem Code dazu führt, dass ein Task das Semaphore öfter released, als er es acquired hat, steigt der interne Counter über dein ursprüngliches Limit von zehn hinaus. Plötzlich ist dein Concurrency-Schutzschild gebrochen, und du sendest unwissentlich zwölf oder fünfzehn gleichzeitige Requests. Um das zu verhindern, solltest du ein asyncio Bounded Semaphore verwenden. Ein Bounded Semaphore verhält sich exakt wie ein Standard-Semaphore, aber es trackt den Initialwert, den du ihm gegeben hast. Wenn ein fehlerhafter Task versucht, das Semaphore über dieses Startlimit hinaus zu releasen, wirft das Bounded Semaphore sofort einen ValueError. Es crasht früh und laut, anstatt die externe API stillschweigend zu überlasten. Verwende standardmäßig immer ein Bounded Semaphore, es sei denn, du hast einen sehr spezifischen architektonischen Grund, deine Concurrency-Limits dynamisch zu erhöhen. Bounded Semaphores fangen logische Release-Fehler in dem Moment ab, in dem sie passieren, halten deine API-Connection-Limits strikt und sorgen dafür, dass deine Systeme vorhersehbar laufen. Das war's für diese Folge. Danke fürs Zuhören und keep building!
11

Producer-Consumer Workflows

3m 58s

Entkopple schnelle Producer sicher von langsamen Consumern. Erkunde asyncio.Queue, die Signalisierung von Task-Abschlüssen und die neuen Shutdown-Mechanismen für Queues.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 11 von 20. Du hast einen async Webserver, der tausende Requests pro Sekunde verarbeitet, und für jeden Request musst du einen Log-Eintrag auf die Festplatte schreiben. Wenn dein Server wartet, bis dieser Schreibvorgang fertig ist, bevor er antwortet, bricht deine Performance ein. Die zuverlässigste Methode, schnelle Producer von langsamen Consumern in async Python zu entkoppeln, ist direkt in die Standard Library eingebaut. Heute schauen wir uns Producer-Consumer-Workflows mit asyncio Queues an. Einige Entwickler, die vom Multi-Threaded Programming kommen, denken, sie müssten diese Queue in Locks packen, um Race Conditions zu verhindern. Musst du aber nicht. Die asyncio Queue ist speziell für concurrent Tasks designt, die auf einem einzigen Event Loop laufen. Sie ist von Haus aus sicher für diese Tasks. Überlass die thread-safe Queues aus dem Standard Queue Modul dem Threading; nutze die asyncio Version für async. Stell dir die Queue wie eine Pipe vor. Auf der einen Seite hast du Producer, die Items hineinschieben. Auf der anderen Seite hast du Consumer, die Items herausziehen. Nehmen wir nochmal das Logging-Szenario. Dein Web Request Handler ist der Producer. Er empfängt einen eingehenden Request, formatiert ein Log Event und ruft die asynchrone put-Methode der Queue auf. Wenn du beim Erstellen der Queue eine maximale Größe festlegst, bekommst du automatischen Backpressure. Wenn die Queue voll ist, pausiert ein await auf die put-Methode den Producer, bis wieder Platz frei wird. Das verhindert, dass ein extremer Traffic-Spike deinen Arbeitsspeicher überlastet. Auf der anderen Seite der Pipe hast du einen separaten Background Task, der als Consumer agiert. Dieser Task läuft in einer Endlosschleife. Er ruft die asynchrone get-Methode der Queue auf. Wenn die Queue leer ist, legt sich der Consumer sicher schlafen. Der Event Loop weckt ihn genau in dem Moment auf, in dem ein Producer ein neues Log Event in die Pipe wirft. Der Consumer nimmt das Event, schreibt es auf die Festplatte und signalisiert dann, dass dieser spezifische Job abgeschlossen ist, indem er eine Methode namens task done aufruft. Diesen Flow während des Application Teardowns zu managen, ist extrem wichtig. Wenn du deinen Webserver graceful herunterfahren willst, musst du sicherstellen, dass alle Log Events in der Queue auch wirklich auf die Festplatte geschrieben werden. Die Queue hat eine Methode namens join. Wenn du join awaitest, blockiert dein Programm, bis die Anzahl der task done Aufrufe exakt der Anzahl der Items entspricht, die ursprünglich in die Queue gepackt wurden. Das garantiert, dass jedes einzelne Stück Arbeit vollständig verarbeitet wurde. Hier ist der entscheidende Punkt. Python 3.13 hat eine neue Queue-Methode namens shutdown eingeführt. Früher musste man zum sauberen Beenden einer Producer-Consumer-Loop spezielle Sentinel Values übergeben, wie zum Beispiel ein None-Objekt in die Queue zu injizieren, nur um dem Consumer zu sagen, dass er seine Loop verlassen soll. Jetzt kannst du einfach shutdown aufrufen. Wenn du das machst, wird jeder Task, der gerade blockiert ist und auf ein put oder get wartet, sofort von einer QueueShutDown Exception getroffen. Du fängst diese Exception in deinen Worker Tasks ab, räumst deine Ressourcen auf und beendest alles sauber, ganz ohne fehleranfällige Sentinel-Logik. Wenn du ein asyncio-System designst, denk daran, dass Queues nicht nur Datenstrukturen sind. Sie sind Flow-Control-Mechanismen, die Backpressure nativ handhaben und deinen Memory Footprint stabil halten, selbst wenn die Producer viel schneller sind als die Consumer. Das war's für diese Folge. Danke fürs Zuhören und keep building!
12

High-Level Networking mit Streams

4m 12s

Tauche ein in High-Level IO Streams. Wir diskutieren StreamReader, StreamWriter und warum das Weglassen von await writer.drain() den Arbeitsspeicher deines Servers stillschweigend zerstören kann.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 12 von 20. Du sendest Daten über eine Netzwerkverbindung, und dein Loop sieht völlig in Ordnung aus. Aber im Hintergrund frisst deine Application unbemerkt Gigabytes an Memory, bis das System sie killt. Das Problem liegt meist an einer einzigen fehlenden Zeile Code für die Flow Control. Deshalb schauen wir uns heute High-Level Networking mit Streams an. asyncio bietet eine High-Level API für die Arbeit mit Netzwerkverbindungen, ohne dass du dich mit Raw Sockets oder Low-Level Transport Protocols herumschlagen musst. Um eine TCP-Verbindung aufzubauen, nutzt du eine Top-Level Function namens open_connection. Du übergibst ihr einen Host String und einen Port Integer. Sie gibt sofort ein Tuple aus zwei Objects zurück: einen StreamReader und einen StreamWriter. Wenn du einen Server statt eines Clients baust, verwendest du start_server. Du übergibst eine Callback Function, einen Host und einen Port. Jedes Mal, wenn sich ein neuer Client verbindet, triggert asyncio deinen Callback und übergibt ihm einen dedizierten Reader und Writer für genau diese Client-Verbindung. Der StreamReader ist dein Interface zum Empfangen von Daten. Er bietet asynchrone Methoden, um Bytes aus dem Netzwerk zu ziehen. Mit der read Methode kannst du eine bestimmte maximale Anzahl von Bytes lesen. Wenn du zeilenbasierte Protokolle parst, kannst du mit der readuntil Methode bis zu einem bestimmten Separator wie einem Newline lesen. Falls dein Protokoll einen Fixed-Size Header erfordert, kannst du readexactly verwenden. Das wartet, bis exakt diese Anzahl von Bytes angekommen ist. Weil all diese Operationen von Network Traffic und Latency abhängen, pausieren sie die Coroutine. Das heißt, du musst sie awaiten. Der zweite Teil davon ist der StreamWriter. Dieses Object kümmert sich darum, Daten wieder rauszusenden. Du nutzt die write Methode, um Bytes in den Stream zu pushen. Hier ist der entscheidende Punkt. Die write Methode ist eine reguläre Function, keine asynchrone. Du awaitest sie nicht. Wenn du write aufrufst, legst du die Daten nicht sofort auf die Netzwerkleitung. Du packst die Daten lediglich in einen internen asyncio Buffer. Der zugrunde liegende Event Loop versucht im Hintergrund, diesen Buffer ins Netzwerk zu flushen. Genau bei diesem Buffer geraten Developer in Schwierigkeiten. Stell dir einen TCP Client vor, der eine riesige File Payload an einen langsamen Server sendet. Wenn du deinen write Call in einen Tight Loop packst, der Chunks von einer lokalen Disk liest, wird Python das File viel schneller lesen, als das Netzwerk es übertragen kann. Weil write deinen Code nicht blockiert, dreht sich dein Loop immer weiter. Der interne Buffer absorbiert das komplette File und frisst den gesamten verfügbaren System Memory auf. Hier kommt Backpressure ins Spiel. Um die Flow Control zu managen, musst du deine write Calls mit der drain Methode paaren. Die drain Methode ist asynchron, das heißt, du awaitest sie. Wenn du drain awaitest, sagst du dem Event Loop, dass er deine Coroutine pausieren soll, falls der interne Buffer seine High-Water Mark überschritten hat. Dein Code wartet, bis der Background Process genug Daten über das Netzwerk gepusht hat, um den Buffer wieder auf eine sichere Größe schrumpfen zu lassen. Das Netzwerk bekommt Zeit aufzuholen, der Buffer leert sich, und deine Memory Usage bleibt flach. Wenn du mit dem Senden deines Files fertig bist, rufst du die close Methode auf dem Writer auf. Genau wie write ist auch close keine Async Function. Um sicherzustellen, dass die Connection wirklich sauber herunterfährt und alle letzten Bytes geflusht werden, bevor dein Programm weiterläuft, awaitest du danach die wait_closed Methode. Der StreamWriter lässt das Schreiben ins Netzwerk zwar verzögerungsfrei wirken, aber die Physik gilt trotzdem noch. Awaite immer drain, nachdem du schreibst, um sicherzustellen, dass deine Application die tatsächliche Geschwindigkeit der Netzwerkverbindung respektiert. Danke fürs Zuhören, Happy Coding zusammen!
13

Async Server bauen

4m 26s

Konstruiere hochgradig nebenläufige Netzwerkserver. Lerne, wie asyncio.start_server Client-Verbindungen abstrahiert und für jeden Peer einen isolierten Task erzeugt.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 13 von 20. Einen hochgradig parallelen TCP-Server in Python zu bauen, bedeutet normalerweise, dass du dich mit Thread-Pools oder komplexen Event-Loop-Konfigurationen herumschlagen musst. Tatsächlich kannst du Tausende von Verbindungen in weniger als zehn Zeilen Code handhaben. Genau das schauen wir uns heute an, indem wir async Server mit asyncio Streams bauen. Die Grundlage eines Netzwerk-Servers in asyncio ist eine Funktion namens start_server. Du übergibst ihr drei Dinge: eine Callback-Funktion, eine IP-Adresse und einen Port. Wenn du start_server mit await aufrufst, bindet sie sich an diese Adresse und beginnt, auf eingehende TCP-Verbindungen an dem von dir angegebenen Network Interface zu lauschen. Entwickler gehen oft davon aus, dass sie diese eingehenden Verbindungen manuell abfangen und Boilerplate-Code schreiben müssen, um sie an Worker-Threads oder eigene Background-Tasks weiterzuleiten. Das ist völlig unnötig. Das Framework übernimmt die Concurrency für dich. Jedes einzelne Mal, wenn sich ein neuer Client mit deinem Port verbindet, spawnt start_server automatisch einen brandneuen asyncio-Task, der komplett diesem spezifischen Client gewidmet ist. Stell dir vor, du baust einen einfachen Chatroom-Server. Wenn sich dein erster User verbindet, triggert start_server deine Callback-Funktion und übergibt ihr zwei Objekte: einen Stream Reader und einen Stream Writer. Wenn sich fünfzig weitere User gleichzeitig verbinden, fahren sofort fünfzig separate Tasks hoch, um genau dieselbe Callback-Funktion auszuführen. Jeder Task erhält sein eigenes, isoliertes Reader- und Writer-Paar. Innerhalb deiner Callback-Funktion schreibst du die Logik so, als würdest du immer nur mit einer Person gleichzeitig sprechen. Du nutzt das Reader-Objekt, um auf eingehende Nachrichten zu lauschen. Du rufst die read-Methode auf dem Reader mit await auf und gibst eine maximale Anzahl von Bytes an, die du akzeptieren willst, zum Beispiel hundert Bytes. Der Reader gibt dir rohe Bytes aus dem Netzwerk, die du in einen Standard-Text-String decodierst. Um dem Client zu antworten, drehst du den Prozess um. Du encodierst deinen Response-String zurück in Bytes und übergibst ihn direkt an das Writer-Objekt. Hier ist die entscheidende Erkenntnis: Daten an den Writer zu übergeben, ist keine asynchrone Operation, aber sicherzustellen, dass diese Daten die physische Maschine auch wirklich verlassen, schon. Nachdem du dem Writer Daten gegeben hast, musst du die drain-Methode des Writers mit await aufrufen. Das Draining pausiert deinen aktuellen Client-Task, bis der Network Buffer des Betriebssystems genug freien Platz hat, um die Bytes über die Leitung zu pushen. Dieser Schritt ist extrem wichtig, weil er verhindert, dass dein Server den gesamten verfügbaren Memory verbraucht, falls ein Client eine langsame Netzwerkverbindung hat. Wenn die Unterhaltung beendet ist oder der Client die Verbindung trennt, sagst du dem Writer, dass er schließen soll. Danach rufst du die wait_closed-Methode des Writers mit await auf, um sicherzustellen, dass alle letzten Bytes übertragen werden und der zugrundeliegende Socket sauber herunterfährt. Zurück in deiner Main-Setup-Funktion hat start_server ein Server-Objekt zurückgegeben. Standardmäßig hört der Server auf zu lauschen, wenn das Main Python Script das Ende seiner Anweisungen erreicht. Um deinen Chatroom auf unbestimmte Zeit offen zu halten, nimmst du dieses Server-Objekt und rufst seine serve_forever-Methode mit await auf. Das sperrt den Main asyncio-Task in eine Infinite Loop, die im Hintergrund still und heimlich neue Verbindungen akzeptiert und neue Client-Tasks spawnt. Die wahre Stärke dieses Designs ist, dass es die Networking-Komplexität abstrahiert. Du schreibst simplen, sequenziellen Code für eine einzelne isolierte Verbindung, und die Event Loop skaliert ihn automatisch über concurrent Tasks hinweg. Wenn du die Show unterstützen möchtest, kannst du auf Patreon nach DevStoriesEU suchen. Das war's für diese Folge. Danke fürs Zuhören und keep building!
14

Nicht-blockierende Subprocesses

3m 39s

Führe Shell-Befehle asynchron aus. Entdecke, warum die Verwendung des Standard-subprocess-Moduls den Event Loop anhält und wie asyncio.create_subprocess_exec dies behebt.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 14 von 20. Du baust eine asynchrone Web-API, triggerst einen Standard-Systembefehl innerhalb eines Endpoints, und plötzlich werden alle anderen concurrent Tasks sofort lahmgelegt. Nichts geht mehr, bis dieser Systembefehl abgeschlossen ist. Der Übeltäter ist das Standard-Python-Modul subprocess, und um das zu lösen, brauchst du Non-blocking Subprocesses. Wenn du eine Funktion wie das Standard subprocess dot run aufrufst, wird ein Betriebssystembefehl ausgeführt und auf dessen Abschluss gewartet. In einer asynchronen Python-Anwendung läuft der Event Loop in einem einzigen Thread. Wenn du diesen Thread blockierst, weil du auf das Betriebssystem wartest, stoppt der Event Loop. Jeder andere concurrent Request an deine API friert ein. Um das zu beheben, bietet asyncio eigene Subprocess-Funktionen, die speziell für den Event Loop entwickelt wurden. Das wichtigste Tool dafür ist asyncio dot create subprocess exec. Hier ist die entscheidende Erkenntnis. Diese Funktion führt den Befehl nicht direkt in Python aus. Sie fordert das Betriebssystem auf, einen Child Process zu spawnen, aber anstatt zu blockieren und auf das Ergebnis zu warten, gibt sie die Kontrolle sofort wieder an den Event Loop zurück. Deine API bearbeitet andere Requests, während das externe Programm läuft. Nehmen wir das Szenario einer Web-API, die Videodateien mit FFmpeg konvertiert. Du möchtest die Konvertierung triggern und die Output-Logs in Echtzeit an den User streamen. Innerhalb deines async Endpoints rufst du create subprocess exec auf. Du übergibst den Programmnamen, FFmpeg, gefolgt von seinen Argumenten. Um die Logs abzufangen, sagst du der Funktion, dass sie den Standard Output und den Standard Error an asyncio Pipes routen soll. Die Funktion gibt ein asyncio Process Objekt zurück. Dieses Objekt repräsentiert den laufenden OS-Befehl und gibt dir asynchrone Hooks, um mit ihm zu interagieren. Weil du die Outputs an Pipes geroutet hast, stellt das Process Objekt sie als asynchrone Stream Reader bereit. Du liest die FFmpeg-Logs, indem du asynchron über den Standard Error Stream iterierst, da FFmpeg normalerweise dorthin loggt. Für jede Zeile, die der externe Process produziert, wacht dein async Loop auf, liest die Zeile und streamt sie zurück an den Web-User. Während er auf die nächste Zeile wartet, geht der Python Event Loop direkt wieder dazu über, andere User zu bedienen. Du bekommst Echtzeit-Log-Streaming, ohne den Server einzufrieren. Wenn du den Output nicht Zeile für Zeile streamen musst, bietet das Process Objekt auch eine async communicate Methode. Du nutzt await communicate, um Daten an den Standard Input zu senden und alle Standard Output und Standard Error Daten auf einmal zu lesen. Das hält den Loop frei, bis der externe Process komplett abgeschlossen ist und die Daten zurückgibt. Wenn du die Streams wie im FFmpeg-Beispiel manuell verarbeitet hast, machst du stattdessen ein await auf die wait Methode des Process Objekts, um darauf zu warten, dass der Process terminiert, und seinen Exit Code abzufragen. Dem Event Loop ist es egal, ob das Betriebssystem die eigentliche Berechnung durchführt; wenn dein Python-Code synchron auf die Rückmeldung des OS wartet, ist deine komplette asynchrone Anwendung lahmgelegt. Das war es für diese Folge. Danke fürs Zuhören und viel Spaß beim Entwickeln!
15

Futures: Die Low-Level-Brücke

3m 53s

Entpacke das Fundament von await Statements. Wir untersuchen asyncio.Future, seine Rolle als zukünftiges Ergebnis und wie es alten Callback-Code mit moderner Syntax verbindet.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 15 von 20. Du schreibst sauberen, modernen asynchronen Code, musst aber irgendwann mit einer alten, hartnäckigen Library interagieren, die komplett auf Callbacks basiert. Du kannst einen Callback nicht direkt awaiten, was deinen gesamten asynchronen Flow unterbricht. Der Mechanismus, der diese beiden Welten verbindet, sind Futures: Die Low-Level-Bridge. Lass uns gleich mit einem häufigen Missverständnis aufräumen. Leute verwechseln oft Tasks und Futures. Ein Task ist eine spezifische Subclass eines Futures. Ein Task wrappt eine Coroutine und schedult sie aktiv auf dem Event Loop, und treibt ihre Ausführung Schritt für Schritt voran. Ein Future führt nichts aus. Es hat keine eigene Ausführungslogik. Es ist einfach nur ein State Container. Es ist ein Low-Level-Primitive, das ein zukünftiges Result einer asynchronen Operation repräsentiert. Wenn du modernes Python schreibst, instanziierst du ein Future fast nie direkt. Der Event Loop erzeugt sie im Hintergrund. Aber wenn du Legacy-Code wrappen musst, der auf Callbacks basiert, erstellst du sie manuell. Stell dir ein Szenario vor, in dem du eine ältere Netzwerkprotokoll-Library verwendest. Sie hat eine request Methode, die eine Netzwerkadresse, einen Success-Callback und einen Failure-Callback entgegennimmt. Du möchtest, dass deine moderne async Funktion einfach await auf diesen Request aufruft. So schließt du diese Lücke. Innerhalb deiner async Funktion holst du dir den aktuell laufenden Event Loop und sagst ihm, er soll ein neues Future Objekt erstellen. Genau in diesem Moment ist das Future in einem pending State. Es ist leer und wartet. Als Nächstes schreibst du eine kleine Success-Callback-Funktion. Wenn sie getriggert wird, nimmt diese Funktion die eingehenden Daten und ruft die set result Methode auf deinem Future auf. Du schreibst außerdem einen Error-Callback, der die set exception Methode auf demselben Future aufruft. Du übergibst diese beiden Funktionen an die Legacy request Methode und startest den Netzwerkaufruf. Schließlich awaitest du das Future. Hier ist die entscheidende Erkenntnis. Das Awaiten eines pending Futures pausiert die aktuelle Coroutine. Es gibt die Kontrolle an den Event Loop zurück, sodass andere Tasks laufen können. Dein Code bleibt an diesem await Statement eingefroren. Währenddessen macht der Legacy-Client seinen Netzwerk-Input und -Output im Hintergrund. Sobald die Daten eintreffen, triggert der Legacy-Client deinen Success-Callback. Dein Callback ruft set result auf dem Future auf. Das Future wechselt sofort vom pending State in den finished State. Der Event Loop bemerkt diese State-Änderung. Er weckt die Coroutine auf, die auf dieses Future gewartet hat, entpackt das gespeicherte Result, und deine async Funktion setzt die Ausführung fort, genau so, als hätte sie eine native Coroutine awaited. Falls der Netzwerkaufruf fehlgeschlagen ist, setzt dein Error-Callback stattdessen eine Exception auf dem Future. Wenn der Event Loop die Coroutine aufweckt, raist er genau diese Exception in der await Zeile. Ein Future hat strenge Regeln bezüglich seines States. Es kann den pending State nur einmal verlassen. Wenn ein Callback versucht, set result auf einem Future aufzurufen, das bereits finished ist, raist Python einen invalid state error. Du kannst ein Future auch manuell canceln. Wenn du das tust, geht es in einen cancelled State über, und jede Coroutine, die es awaitet, erhält sofort einen asyncio Cancelled Error. Futures bieten den nötigen strukturellen Kleber zwischen event-driven Callbacks und prozedural aussehenden await Statements. Zu verstehen, dass jedes await Statement letztendlich die Ausführung pausiert, bis ein Low-Level-Future als finished markiert ist, gibt dir absolute Klarheit darüber, wie asynchrones Python unter der Haube tatsächlich funktioniert. Das war's für diese Folge. Danke fürs Zuhören und keep building!
16

Transports und Protocols

4m 39s

Schau unter die Haube, um zu sehen, wie asyncio mit dem Betriebssystem kommuniziert. Verstehe die Callback-gesteuerte 1:1-Beziehung zwischen Transports (wie sich Bytes bewegen) und Protocols (was Bytes bedeuten).

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 16 von 20. Wenn du High-Level asyncio Streams nutzt, sieht dein Code sauber und sequenziell aus und wird sicher awaited. Aber unter diesen freundlichen Coroutines sitzt eine stark optimierte, callback-gesteuerte Engine, die sich mit chaotischen Betriebssystem-Calls herumschlägt. Um zu verstehen, wie deine Python-Application tatsächlich mit einem Netzwerk kommuniziert, musst du dir Transports und Protocols ansehen. Diese beiden Abstraktionen bilden das Fundament des asyncio Networking. Sie arbeiten immer als Paar. Das Protocol kümmert sich um die Application Logic und entscheidet, welche Bytes gesendet und wie eingehende Daten interpretiert werden. Der Transport übernimmt die Mechanik. Ihm ist völlig egal, was deine Daten bedeuten oder wie sie formatiert sind. Seine einzige Aufgabe ist herauszufinden, wie diese Bytes übers Netzwerk gepusht werden. Heute konzentrieren wir uns voll und ganz auf den Transport Layer. Denk mal darüber nach, was passiert, wenn du direkt in einen non-blocking TCP-Socket schreibst. Du musst das Betriebssystem fragen, ob der Socket ready ist. Du musst Partial Writes handlen, wenn der Network Buffer voll ist. Du musst tracken, welche Bytes tatsächlich gesendet wurden und welche später noch mal probiert werden müssen. Ein asyncio Transport versteckt all diese Komplexität. Er fungiert als opaker Wrapper um den Raw Socket und die zugrundeliegenden Betriebssystem-Calls. Normalerweise instanziierst du einen Transport nie selbst. Stattdessen rufst du eine Event Loop Methode auf, um eine Netzwerkverbindung zu erstellen. Der Event Loop richtet den Socket ein, erstellt den Transport, verlinkt ihn mit deinem Protocol und gibt dir das Paar zurück. Hier ist die entscheidende Erkenntnis: Sobald diese Connection steht, übernimmt der Transport das Input und Output Buffering. Wenn dein Protocol eine Message senden will, übergibt es einfach einen Chunk von Bytes an die Write-Methode des Transports. Der Transport blockiert deinen Code nicht, um aufs Netzwerk zu warten. Er droppt diese Bytes sofort in seinen eigenen internen Buffer. Der Transport arbeitet dann im Hintergrund mit dem Event Loop zusammen und feuert die non-blocking Socket-Calls an das Betriebssystem ab. Wenn das System gerade nur die Hälfte der Bytes aufnehmen kann, behält der Transport den Rest und versucht es in der nächsten Loop Iteration noch mal. Deine Application muss diese Queue niemals micromanagen. Flow Control ist direkt in diesen Mechanismus eingebaut. Wenn du Daten schneller schreibst, als das Netzwerk sie senden kann, fängt der interne Buffer des Transports an, sich zu füllen. Sobald ein bestimmtes Limit erreicht ist, triggert der Transport einen spezifischen Callback auf deinem Protocol, um das Schreiben zu pausieren. Wenn der Buffer schließlich leergelaufen ist, feuert er einen weiteren Callback, um weiterzumachen. Auf der Empfangsseite hört der Transport auf den Event Loop. Wenn das Betriebssystem signalisiert, dass eingehende Bytes angekommen sind, zieht der Transport sie aus dem Socket und füttert sie über einen Callback direkt in das Protocol. Auf dieser Low-Level-Ebene ist alles rein callback-gesteuert. Hier gibt es keine Awaitables. Transports bieten außerdem standardisierte Methoden, um den Connection Lifecycle zu managen. Du kannst einen Transport gracefully schließen, was ihm sagt, dass er alle buffered Daten zu Ende senden soll, bevor er den Socket sicher herunterfährt. Wenn etwas schiefgeht, kannst du eine Abort-Methode aufrufen, um die Connection sofort abzubrechen und alles zu verwerfen, was noch in der Queue ist. Und wenn dein Protocol wissen muss, mit wem es spricht, bietet der Transport eine Methode, um Extra-Informationen anzufordern. So kannst du durch die Abstraktion hindurchschauen und die zugrundeliegende Socket-IP-Adresse oder Peer-Details abrufen. Die Transport-Abstraktion ist das, was es deinem asyncio-Code ermöglicht, sich rein auf die Data Logic zu fokussieren. Transports isolieren deine Application von der chaotischen Mechanik von non-blocking I/O; sie nehmen Raw Bytes von deinem Protocol und kümmern sich still und heimlich um das Buffering, die Retries und die Betriebssystem-Socket-Calls, die nötig sind, um sie übers Netzwerk zu bewegen. Das war's für diese Folge. Danke fürs Zuhören, und keep building!
17

Threading in einer Async-Welt

3m 27s

Verbinde synchrone und asynchrone Welten. Lerne, wie man schweren blockierenden Code mit Executors und Thread-sicheren Callbacks sicher auslagert, ohne den Loop zu blockieren.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 17 von 20. Du packst einen Standard Background Thread in deinen async Webserver, um einen langsamen Task zu verarbeiten, und plötzlich fängt deine Anwendung an, Deadlocks zu produzieren oder kryptische State Errors zu werfen. Standard Threads mit einer async Event Loop zu mischen, ist ein Rezept für ein Desaster, es sei denn, du nutzt die dafür vorgesehenen thread-safe Bridges. Heute behandeln wir Threading in einer async Welt. Die wichtigste Regel von asyncio ist, dass die Event Loop in einem einzigen Thread läuft. Deswegen sind fast alle asyncio Objekte nicht thread-safe. Ein häufiger Fehler ist es, einen Standard Background Thread zu starten, etwas Arbeit zu erledigen und dann zu versuchen, ein async Future zu resolven oder einen Callback direkt aus diesem Thread zu schedulen. Wenn du ein asyncio Objekt von einem anderen Thread aus anfasst als dem, der die Event Loop ausführt, zerstörst du den Loop State. Um eine Nachricht von einem Background Thread an deine Event Loop zu senden, musst du call soon threadsafe verwenden. Das ist eine Methode der Loop selbst. Du übergibst ihr die Callback-Funktion, die du ausführen willst, und die Argumente. Anstatt sie sofort auszuführen, packt dein Background Thread diesen Callback in eine sichere interne Queue. Die Main Event Loop prüft diese Queue und führt deinen Callback während ihres normalen Zyklus sicher im Main Thread aus. Das ist der einzige sichere Weg für einen externen Thread, die Event Loop anzustupsen. Schauen wir uns nun den umgekehrten Fall an. Du lässt deine async Event Loop laufen und musst ein Stück synchronen, blockierenden Code ausführen. Ein klassisches Szenario ist die Abfrage eines langsamen, synchronen PostgreSQL Drivers wie psycopg2. Wenn du eine fünfsekündige Datenbankabfrage direkt in deinem async Request Handler ausführst, bleibt dein gesamter Webserver stehen. Die Event Loop kann keinen anderen Network Traffic oder Timer verarbeiten, bis diese Datenbankabfrage zurückkehrt. Hier ist die entscheidende Erkenntnis. Um zu verhindern, dass die Loop einfriert, lagerst du diese blockierende Arbeit mit run in executor in einen separaten Thread aus. Das ist eine weitere Methode der Event Loop. Du übergibst ihr einen Thread Pool Executor und deine synchrone Datenbankfunktion. Die Loop übergibt die Funktion an einen Background Thread im Pool und gibt sofort ein Awaitable Object zurück. Du awaitest dieses Objekt. Während deine Datenbankabfrage im Background Thread läuft, ist deine Event Loop völlig frei, diesen spezifischen Task zu pausieren und Hunderte anderer Web Requests zu bearbeiten. Sobald der PostgreSQL Driver die Daten endlich zurückgibt, reicht der Thread Pool das Ergebnis sicher an die Event Loop zurück. Dein Awaitable resolved, und deine ursprüngliche async Funktion setzt die Ausführung genau dort fort, wo sie aufgehört hat, und hält nun die Datenbankergebnisse. Du hast zwei One-Way Bridges. Nutze call soon threadsafe, um Events von einem Worker Thread in deine async Loop zu pushen. Nutze run in executor, um blockierende synchrone Arbeit aus deiner async Loop in einen Worker Thread auszulagern. Lass niemals zu, dass ein synchroner Call deine Event Loop kapert, und lass niemals einen Background Thread direkt deine async Objekte anfassen. Das war's für diese Folge. Danke fürs Zuhören, und keep building!
18

Async Generators & Cleanup

3m 41s

Vermeide Ressourcenlecks mit Async Generators. Wir untersuchen, warum eine 'async for'-Iteration bei Unterbrechung hängende Verbindungen hinterlassen kann und wie aclosing() für Sicherheit sorgt.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 18 von 20. Du bekommst einen Timeout beim Abrufen von Rows aus einer Datenbank. Dein Code fängt die Exception ab und macht weiter, aber Tage später stürzt deine Applikation ab, weil dein Connection Pool komplett erschöpft ist. Du hast eine asynchrone Loop vorzeitig verlassen, und sie hat im Hintergrund unbemerkt Datenbankverbindungen offen gelassen. Die Lösung liegt darin, Async Generators und Cleanup zu meistern. Wenn du einen Async Generator schreibst, um Items über die Zeit zu yielden, verwaltest du oft Ressourcen. Nimm zum Beispiel einen Datenbank-Cursor. Du schreibst einen Generator, der sich eine Connection holt, Rows nacheinander yieldet und einen try-finally Block nutzt, um diese Connection an den Pool zurückzugeben, wenn das Fetchen abgeschlossen ist. Wenn du über jede einzelne Row iterierst, ist der Generator irgendwann fertig, erreicht den finally Block und macht den Cleanup. Alles funktioniert. Gefährlich wird es, wenn du den Generator nicht komplett konsumierst. Wenn deine Iteration in einen Timeout läuft, oder wenn du einfach ein break Statement erreichst, nachdem du die gesuchte Row gefunden hast, pausiert der Generator. Er ist beim letzten yield suspendiert. Er hat den finally Block nicht erreicht. Deine Datenbankverbindung wird weiterhin offen gehalten. Du erwartest vielleicht, dass der Garbage Collector von Python das irgendwann übernimmt. In synchronem Code injiziert Python eine Exit Exception, die die finally Blöcke ausführt, wenn ein Generator alle Referenzen verliert und vom Garbage Collector abgeräumt wird. Aber asynchroner Code macht das komplizierter. Garbage Collection ist ein synchroner Prozess. Wenn der Garbage Collector schließlich deinen suspendierten Async Generator findet, kann er asynchronen Teardown-Code nicht zuverlässig ausführen. Die Event Loop könnte beschäftigt oder sogar schon geschlossen sein. Sich beim Cleanup eines Async Generators auf den Garbage Collector zu verlassen, führt zu unvorhersehbarem Verhalten und Dangling Resources. Das ist der entscheidende Punkt. Die offizielle asyncio Dokumentation sagt ausdrücklich, dass du dich für den Async Generator Cleanup niemals auf die Garbage Collection verlassen solltest. Du musst sie manuell schließen. Die Standard Library bietet dafür ein direktes Tool namens aclosing, das du im contextlib Modul findest. Es fungiert als asynchroner Context Manager. Seine einzige Aufgabe ist es zu garantieren, dass die aclose Methode des Generators aufgerufen und awaited wird, sobald du damit fertig bist. Anstatt deinen Generator direkt in eine async for Loop zu packen, wrappst du ihn. Zuerst erstellst du die Generator-Instanz. Dann übergibst du sie an ein async with aclosing Statement. Innerhalb dieses Context Blocks führst du deine async for Loop aus. Wenn du deinen Code so strukturierst, triggert ein Early Exit den Context Manager. Wenn ein Timeout die Loop unterbricht, fängt der async with Block den Exit ab. Er awaitet explizit die aclose Methode auf dem Generator. Das injiziert die Exit Exception sicher in den suspendierten Generator, während du noch aktiv in der Event Loop läufst. Dein finally Block wird sofort ausgeführt, awaitet alle notwendigen Teardown-Schritte, und deine Datenbankverbindung geht sicher zurück in den Pool. Wann immer ein Async Generator Netzwerkverbindungen, File Descriptors oder Database Locks anfordert, wrappe ihn vor dem Iterieren in aclosing, um einen deterministischen Cleanup zu garantieren, unabhängig von Timeouts oder Early Breaks. Das war's für diese Folge. Danke fürs Zuhören und keep building!
19

Den Debug-Modus meistern

3m 51s

Finde Concurrency-Bugs sofort. Lerne, wie man PYTHONASYNCIODEBUG nutzt, um langsame Callbacks zu profilen, nicht mit await aufgerufene Coroutines aufzudecken und nie abgerufene Exceptions genau zu lokalisieren.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 19 von 20. Dein Production-Server leidet unter unerklärlichen Lag-Spikes, und deine Background-Operations verschlucken zufällig Fehler ohne jede Spur. Das Problem ist nicht deine Application-Logic, sondern wie Standard-asyncio Concurrency-Fehler verbirgt, um Performance zu sparen. Den Debug Mode zu beherrschen, ist die Antwort, um diese Fehler sofort aufzudecken. Der asyncio Debug Mode funktioniert wie ein Strict Mode für den Event Loop. Standardmäßig priorisiert asyncio reine Geschwindigkeit gegenüber Runtime Safety Checks. Das bedeutet, wenn Dinge schiefgehen, schlagen sie oft stillschweigend fehl. Du aktivierst den Debug Mode global, indem du die Environment-Variable PYTHONASYNCIODEBUG auf eins setzt, oder indem du Python mit dem dash X dev Flag ausführst. Du kannst ihn auch dynamisch einschalten, indem du set debug true direkt auf dem Event Loop Objekt aufrufst. Nehmen wir das Lag-Spike-Szenario. Du hast einen Webserver, der Tausende von concurrent Requests verarbeitet, und plötzlich bringt ein einzelner Endpoint die gesamte Application zum Einfrieren. Du vermutest, dass eine außer Kontrolle geratene Regex-Operation den Thread blockiert, aber das Standard-Logging sagt dir nur, wann ein Request startet oder endet, nicht, was den Loop dazwischen blockiert hat. Wenn der Debug Mode aktiv ist, misst der Event Loop die Execution Time von jedem einzelnen Callback. Wenn ein Callback den Loop für mehr als einhundert Millisekunden blockiert, loggt asyncio automatisch eine Warnung. Diese Warnung enthält die genaue Datei und Zeilennummer, wo der Freeze aufgetreten ist, und führt dich direkt zu dieser teuren Regex-Suche. Dieser Schwellenwert von einhundert Millisekunden ist der Default, aber du kannst ihn für deine spezifischen Latency-Anforderungen anpassen, indem du die slow callback duration Property auf dem Loop modifizierst. Der Debug Mode fängt auch silent Execution-Failures ab. Ein häufiger Fehler in async Code ist der Aufruf einer Coroutine-Funktion, bei dem man aber das await Keyword vergisst. Die Funktion gibt ein Coroutine-Objekt zurück, aber die eigentliche Logik wird nie ausgeführt. Bei normaler Execution wird dieses Objekt stillschweigend verworfen. Der Debug Mode trackt das. Wenn der Garbage Collector eine unawaited Coroutine aufräumt, fängt der Debug Loop sie ab und gibt eine Resource Warning aus. Diese zeigt genau, wo die verwaiste Coroutine erstellt wurde, damit du die Invocation fixen kannst. Dasselbe Sicherheitsnetz gilt für Background Tasks. Wenn ein asyncio Task crasht, wird die Exception im Task-Objekt selbst gespeichert. Wenn dein Code diesen Task nie explizit awaited oder sein Result abruft, verschwindet die Exception einfach. Wenn der Debug Mode aktiviert ist, überwacht asyncio den Lifecycle von jedem Task. Wenn ein Task zerstört wird und seine interne Exception nie abgerufen wurde, loggt der Event Loop den Error lautstark zusammen mit dem Traceback, der zeigt, wo der Task ursprünglich gespawnt wurde. Diese Checks erzeugen durchaus Overhead, daher lässt du den Debug Mode in normalen Production-Environments typischerweise aus und sparst ihn dir für die lokale Development-Umgebung oder gezieltes Troubleshooting auf. Hier ist die wichtigste Erkenntnis: Den Debug Mode zu aktivieren, verlagert die Last, silent Concurrency-Bugs zu finden, von deinem eigenen manuellen Logging direkt zurück auf den Event Loop selbst. Wenn dir der Podcast gefällt und du uns unterstützen möchtest, such einfach auf Patreon nach DevStoriesEU. Das war es für diese Folge. Danke fürs Zuhören, und keep building!
20

Erweitern & Custom Loops

4m 09s

Das Finale. Wir erkunden fortgeschrittene Integration und was nötig ist, um einen Custom Event Loop zu schreiben oder BaseEventLoop für spezialisierte, hochperformante Umgebungen abzuleiten.

Herunterladen
Hallo, hier ist Alex von DEV STORIES DOT EU. asyncio, Folge 20 von 20. Du stößt mit deinem asynchronen Code an eine Performance-Grenze, und das Profiling zeigt direkt auf den Core Event Loop selbst. Du kannst die Standard Library nicht neu schreiben, brauchst aber Lower-Level-Kontrolle darüber, wie genau das System Sockets und Tasks verarbeitet. Die Antwort liegt im Erweitern und in Custom Loops. Der Standard-Event-Loop von asyncio ist keine hardcodierte Blackbox. Er ist ein erweiterbares Interface. Er wurde von Grund auf so designt, dass er komplett durch High-Performance-C-Libraries oder spezialisierte Python-Implementierungen ersetzt werden kann. Die meisten Application-Developer werden nie einen Custom Loop bauen müssen. Wenn du aber ein Framework-Autor bist oder einen optimierten Loop wie uvloop baust, musst du das Standardverhalten umgehen und direkt mit Lower-Level-Betriebssystem-Primitiven integrieren. Um einen Custom Event Loop zu bauen, startest du damit, eine Subclass von BaseEventLoop zu erstellen. Diese Base Class definiert den kompletten Contract dafür, wie sich asynchrone Operationen verhalten müssen. Indem du davon erbst, bekommst du die Struktur, aber du kannst spezifische Methoden overriden, um grundlegende Operationen abzufangen und neu zu definieren. Nehmen wir die Socket-Erstellung. In einer Standardanwendung bittest du asyncio, eine Connection zu öffnen, und es nutzt die Default-Python-Socket-Implementierung. Aber in einer Custom-Loop-Subclass kannst du die Methoden zur Netzwerkerstellung overriden. Das bedeutet: Wenn die Application eine Network Connection anfordert, fängt dein Custom Loop diesen Call ab. Du kannst diesen Request dann durch hochoptimierten C-Code routen oder ihn direkt an erweiterte Kernel-Features anbinden, die Standard-Python nicht bereitstellt. Der Application-Code ändert sich nicht, aber die zugrundeliegende Maschinerie gehört komplett dir. Diese granulare Kontrolle gilt auch für das Task-Management. Hier ist die wichtigste Erkenntnis. Der Event Loop ist dafür verantwortlich, jeden einzelnen asynchronen Task zu tracken. Wenn du unter die Haube von BaseEventLoop schaust, findest du eine interne Methode namens underscore register task. Indem du diese spezifische Methode overridest, fängt dein Custom Loop einen Task in genau der Mikrosekunde ab, in der er erstellt wird. Warum ist das wichtig? Wenn du eine Custom Runtime baust, musst du vielleicht tiefgehende Diagnostics tracken, spezielles Memory-Pooling für Tasks implementieren oder den Task State direkt an einen Custom-Monitoring-Service senden. Das Overriden von underscore register task gibt dir einen garantierten Hook in den Lifecycle jeder Coroutine, noch bevor sie überhaupt ausgeführt wird. Du kannst auch die entsprechende unregister-Methode overriden, um das Cleanup exakt so zu handhaben, wie dein Framework es verlangt. Sobald deine Custom-Loop-Klasse gebaut ist, musst du Python sagen, dass es sie auch wirklich nutzen soll. Das machst du, indem du eine Custom Event Loop Policy erstellst. Die Policy ist einfach eine Factory, die diktiert, welche Loop-Implementierung erstellt wird, wenn ein Thread danach fragt. Du setzt deine Custom Policy global. Ab diesem Zeitpunkt bekommt jede Standard-Library-Funktion, die einen Event Loop anfordert, deine optimierte, Custom-Version übergeben. Die wahre Power von asyncio ist nicht nur die async- und await-Syntax. Es ist die Tatsache, dass die komplette Execution Engine ein pluggable Interface ist, bereit, ausgetauscht zu werden, sobald die Standard-Performance deine Architektur limitiert. Da dies unsere Serie abschließt, ermutige ich dich, die offizielle Documentation zu lesen, hands-on auszuprobieren, diese Komponenten zu erweitern, oder devstories dot eu zu besuchen, um Themen für zukünftige Serien vorzuschlagen. Das war's für diese Folge. Danke fürs Zuhören, und keep building!