0%

Kapitel 11

Ein Vorgeschmack auf Async

Warum die meisten WebSocket-Server asynchron sind

Die meisten -Server, denen du außerhalb dieses Kurses begegnest, sind mit async def geschrieben. Deine Anzeigetafel ist es absichtlich nicht: Threads haben es jedem früheren Kapitel erlaubt, einer Verbindung nach der anderen zu folgen, mit der Art von , die du schon kanntest. Dieses Kapitel ist ein Vorgeschmack auf den anderen Stil, kein Kurs darüber. Du wirst ein wenig asyncio lesen und ausführen, einen Handler als neu schreiben und sehen, wo der Vorgeschmack endet. asyncio richtig zu lernen braucht einen eigenen Kurs: Async Python, der nur Python in der Praxis voraussetzt.

Diese Lektion schreibt keinen Code. Sie fragt, was dein Server aufwendet, während seine Zuschauer warten, und was der andere Stil im Gegenzug verlangt.

Was dein Server aufwendet

Seit Kapitel 7 läuft deine Anzeigetafel auf dem synchronen Server der Bibliothek aus Kapitel 6, der jeder Verbindung einen eigenen Thread gibt. Der Handler läuft in diesem Thread vom bis zum Schließen. Der Handler eines Zuschauers verbringt sein Leben in der , die du in Kapitel 10 fertiggestellt hast: auf die Outbox warten, senden, was sie enthält, wieder warten.

Ein Lauf auf einem lokalen Computer, noch nicht auf der Seite, hat die Threads in einem Serverprozess gezählt. Ohne verbundenen Zuschauer lief der synchrone Server mit 2 Threads: dem Hauptthread und dem, der serve_forever ausführt. Mit 20 untätigen Zuschauern, verbunden aus einem anderen Prozess, waren es 62. Das sind 60 mehr für 20 Zuschauer, drei pro Zuschauer: der Thread, in dem dein Handler läuft, einer, mit dem die Bibliothek die Verbindung liest, und einer für ihr . Also 2 + 3 x 20 = 62. Ein anderer Computer liefert andere Zahlen; worauf es ankommt, ist die Form: Threads wachsen mit den Zuschauern.

Deine synchrone Anzeigetafel hat 20 Zuschauer, die alle auf die nächste Wertung warten. Was tut jeder ihrer Handler-Threads?

Ein Zuschauer wartet meistens

Wie viel dieser Zeit ist Arbeit? Nimm einen zweistündigen Quizabend mit 40 Wertungen und nimm an, dass das Senden eines Snapshots an einen Zuschauer etwa eine Millisekunde dauert. Dieser Block macht die Rechnung für einen Zuschauer:

Try it

Er gibt busy 0.04 s of 7200 s: 0.0006 % aus. Die Millisekunde ist eine grobe Schätzung, und das Zehnfache würde den Zuschauer immer noch fast den ganzen Abend untätig lassen. Warten ist der normale Zustand eines Zuschauers, wie Kapitel 7 herausfand, als es Zuschauer zum ersten Mal zwischen Wertungen verbunden hielt. Ein Thread pro Zuschauer wird während aller 7200 Sekunden bezahlt, während sich die Arbeit auf weniger als eine Zehntelsekunde summiert.

Warum ist es wichtig, dass ein Zuschauer während deutlich weniger als einem Prozent eines Quiz beschäftigt ist?

Eine Schleife, viele Wartevorgänge

Die andere Form hast du schon gesehen. In Networks with Python hat Kapitel 9 viele Clients aus einem Thread mit selectors bedient: Ein Aufruf, select, wartete, bis irgendein bereit war, und die Schleife bediente nur die bereiten, bevor sie wieder wartete. Jede Verbindung behielt ihren eigenen Zustand in einem , weil kein Thread ihn für diese Verbindung hielt.

Eine verallgemeinert diese Idee. Sie hält jede wartende Verbindung geparkt, und wenn eine davon weitermachen kann, weil Bytes angekommen sind oder ein Snapshot zum Senden bereit ist, führt sie den Handler dieser Verbindung aus, bis der Handler wieder warten muss. Dann geht sie zur nächsten, die weitermachen kann. Handler auf einer Loop werden als Coroutinen geschrieben: Funktionen, die mittendrin anhalten, die Loop etwas anderes ausführen lassen und später dort fortgesetzt werden können, wo sie angehalten haben. Genau das definiert async def, und Lektion 2 zeigt, wie. Pythons Standard-Event-Loop lebt im asyncio, und die Bibliothek websockets hat einen darauf gebauten Server, websockets.asyncio.

Derselbe lokale Lauf hat auch die Threads dieses Servers gezählt: 1 mit 20 untätigen Zuschauern und 1 ohne. Das Bild stellt die beiden Formen für zwanzig Zuschauer übereinander. Zähle die Threads in jeder und finde den einen Zuschauer, der überhaupt etwas tut.

Mit einem Thread pro Verbindung brauchen zwanzig Zuschauer 2 + 3 x 20 = 62 Threads, die meistens warten; mit einer Event-Loop sind dieselben zwanzig Zuschauer geparkt, und der eine Thread der Loop führt den einen Zuschauer aus, der weitermachen kann.

In der oberen Hälfte hat jeder der zwanzig Zuschauer drei Threads (den seines Handlers, einen Leser und ein Keepalive), dazu die zwei des Servers, und fast alle warten: 2 + 3 x 20 = 62 Threads. In der unteren Hälfte sind dieselben zwanzig Zuschauer geparkt, und ein Thread führt den Zuschauer aus, der gerade weitermachen kann, hier Zuschauer 3, bevor er zur Loop zurückkehrt. Das ist insgesamt 1 Thread.

Der Preis

Die Loop hat einen Thread, also führt sie einen Handler auf einmal aus. Das funktioniert nur, wenn jeder Handler die Kontrolle an die Loop zurückgibt, sobald er warten muss. Ein Handler, der auf die alte Art wartet, mit einem blockierenden Aufruf wie time.sleep oder dem Event.wait der Outbox, hält den einzigen Thread, und jeder andere Zuschauer wartet mit ihm, bis dieser Aufruf zurückkehrt. Lektion 3 zeigt, wie das passiert, mit einer Zählung.

Keiner der beiden Stile ist also einfach besser. Für die fünf Clients, die dieser Kurs gleichzeitig bedient hat, kosten Threads wenig und lassen dich über eine Verbindung nach der anderen nachdenken. Ein Dienst mit Tausenden von Zuschauern, die meistens warten, ist der übliche Grund, auf eine Loop umzusteigen, weil so viele Threads weit mehr kosten würden als die Arbeit, die sie leisten. Das ist das gängige , nichts, was dieser Kurs gemessen hat.

Welche Situation ist der stärkste Grund, die Anzeigetafel auf eine Event-Loop umzustellen?

In der nächsten Lektion schreibst du deine ersten Coroutinen und führst sie mit asyncio.run auf dem Server aus.