0%

Ein Vorgeschmack auf Async · Übung

Der Zuschauer-Handler als Coroutine

Du kannst jetzt eine schreiben, eine andere abwarten und die äußerste ausführen. Diese Lektion schreibt den Zuschauer-Handler des Abschlussprojekts in diesem Stil neu, und nur den Zuschauer-Handler: Die Tafel, der Broadcast und der Punktezähler bleiben, wo Kapitel 10 sie gelassen hat. Hier ist die aus Kapitel 10, dann die Coroutine, die du schreiben wirst:

Chapter 10, a thread per viewer:
  snapshot = wait_and_take(outbox, CHECK_EVERY)
  nothing yet: notice_close(connection)
  connection.send(text)
  last version sent: connection.close(1000, ...)
  except ConnectionClosed: leave quietly

Chapter 11, a coroutine:
  snapshot = await next_snapshot()
  (next_snapshot waits until there is one)
  await connection.send(text)
  None: await connection.close(1000, ...)
  except ConnectionClosed: leave quietly

Vieles bleibt gleich. Der Beitritts-Snapshot kommt zuerst. Ein Sender pro Zuschauer, der Handler selbst, sendet nur den neuesten Snapshot. Eine Verbindung, die endet, löst ConnectionClosed aus, was den Handler still beendet. Und das Quiz endet auf dieselbe Weise: der letzte Snapshot, dann ein Schließen mit 1000. Was sich ändert, ist jede Stelle, an der der Handler wartet.

Warum nicht Event.wait()

In Kapitel 8 hat wait_and_take mit dem Event.wait(timeout=...) der Outbox gewartet. Dieser Aufruf blockiert: Der Thread, der ihn macht, tut nichts anderes, bis das Event gesetzt ist oder das Timeout abläuft. Mit einem Thread pro Zuschauer war genau das der Sinn. Auf einer ist es eine Katastrophe, weil der Thread, den er blockiert, der einzige Thread der Loop ist, und jeder andere Zuschauer darauf gewartet hat, dass dieser Thread ihn ausführt.

pl_ws.py, ein für diesen Kurs geschriebenes Programm, das jeden Client spielt, zeigt den Unterschied mit zwei eigenen Handlern. Neben jedem zählt eine Ticker-Coroutine alle 0,05 s hoch. In einem Lauf lief der Ticker 9 Mal, während ein Handler mit await asyncio.sleep(0.5) wartete, und 0 Mal, während der andere mit time.sleep(0.5) wartete. Beide haben eine halbe Sekunde gewartet. Nur der erste hat die Kontrolle zurückgegeben, während er wartete.

Warum kann die Coroutine nicht mit dem Event.wait(timeout=...) der Outbox auf den nächsten Snapshot warten?

next_snapshot()

Deshalb bekommt der Handler stattdessen eine Coroutine- zum Abwarten, next_snapshot, und dieses Kapitel liefert sie mit. Das Abwarten von next_snapshot() liefert zuerst den Beitritts-Snapshot, dann jedes Mal den neuesten Snapshot, wenn es einen neueren gibt, und None, sobald das Quiz vorbei ist. Solange nichts Neues angekommen ist, wartet es, ohne zu blockieren: Der Handler hält bei diesem await an, und die Loop bedient andere Zuschauer. Es ist auf asyncio-Werkzeugen gebaut, die dieser Vorgeschmack nicht lehrt, darunter ein asyncio.Event, das man abwarten kann, statt darauf zu warten.

Lies es als die Outbox aus Kapitel 8 und das Ende aus Kapitel 10, von innerhalb des Handlers gesehen. Nur das Neueste ist die Regel der Outbox. None ist die Regel der letzten Version: Der Zuschauer hat den letzten Snapshot bekommen, also schließe mit 1000.

Eines fehlt. Der Handler aus Kapitel 10 hat im Leerlauf nach der Verbindung geschaut, um einen Zuschauer zu bemerken, der gegangen war. Hier wird ein Zuschauer, der geht, beim nächsten Senden bemerkt, das ConnectionClosed auslöst. Die Verbindung zu beobachten und gleichzeitig auf einen Snapshot zu warten, braucht zwei Dinge, die in einem Handler gleichzeitig passieren, und das braucht , und Tasks gehören auf die von Lektion 4.

Drei awaits

In websockets.asyncio sind send, recv und close einer Verbindung Coroutine-Funktionen, also wird jede abgewartet: await connection.send(text), await connection.close(1000, "the quiz is over"). Vergiss das await bei einem Senden, und die Warnung aus Lektion 2 kommt zurück: Der Aufruf hat nur eine Coroutine erstellt, nichts hat sie ausgeführt, und der Zuschauer hat nichts empfangen.

Dein Handler wartet genau drei Dinge ab: next_snapshot(), connection.send(...) und connection.close(...). Wenn du auf Submit klickst, führt die Prüfung deine Coroutine mit Stellvertretern Schritt für Schritt aus, damit sie jede Stelle sehen kann, an der dein Handler die Kontrolle zurückgibt. Weil sie deine Coroutine ohne Event-Loop ausführt, darf dein Handler nur diese drei abwarten; alles abzuwarten, was eine Loop braucht, etwa asyncio.sleep(0.1), wird als Fehler gemeldet.

Dein Handler schreibt connection.send(text) ohne await. Was empfängt der Zuschauer?

Der asyncio-Server, nur zum Lesen

So sieht ein auf websockets.asyncio gebauter Server aus, zum Wiedererkennen. In diesem Kurs wirst du ihn nicht schreiben:

import asyncio

from websockets.asyncio.server import serve


async def handler(connection):
    await connection.send("hello")


async def main():
    async with serve(handler, "127.0.0.1", 0) as server:
        await server.serve_forever()


asyncio.run(main())

Vergleiche ihn mit Kapitel 6. serve nimmt einen Handler mit einem , der Verbindung, wie der synchrone Server, aber der Handler ist eine Coroutine-Funktion. Der with-Block wird zu async with, einem with, dessen Aufbau und Aufräumen abgewartet werden, und serve_forever wird abgewartet statt aufgerufen. Ein einziges asyncio.run ganz unten startet alles. Um jede Nachricht eines Clients zu lesen, schreibt asyncio-Code async for message in connection:, eine for-Schleife, deren jeder Schritt abgewartet wird. Beide Formen erscheinen in diesem Kurs nur in Fragmenten wie diesem.

Deine Aufgabe

Schreibe in async_scoreboard.py viewer_handler(connection, next_snapshot). In einer Schleife: Warte next_snapshot() ab; hat es None geliefert, warte connection.close(1000, "the quiz is over") ab und kehre zurück; andernfalls warte connection.send(...) mit scoreboard_messages.encode_message(snapshot) ab. Packe die Schleife in try und except ConnectionClosed und kehre aus dem except still zurück. Das sind etwa neun Zeilen.

Klicke auf Run. Der erste Abschnitt zu Lektion 3 führt deinen Handler unter asyncio.run aus, mit einer Stellvertreter-Verbindung, deren send und close Coroutine-Funktionen sind, die aufzeichnen, was sie bekommen, und mit einem next_snapshot, das drei Snapshots und dann None liefert. Im zweiten stellt pl_ws.py deinen Handler mit websockets.asyncio drei echten Zuschauern auf bereit und führt dann den Ticker-Vergleich aus. Eine fertige Version hat auf der Seite ausgegeben:

--- Lesson 3: your handler with a stand-in connection
sent version 0
sent version 1
sent version 2
closed 1000 the quiz is over
--- Lesson 3: your handler served by websockets.asyncio
pl_ws.py, a program written for this course, serves your handler with websockets.asyncio and plays three viewers:
scenario async-viewers: your async viewer_handler served by websockets.asyncio; three viewers; versions 1 to 3, then the end of the quiz
viewer A received versions [0, 1, 2, 3], then the server closed it with 1000 'the quiz is over'
viewer B received versions [0, 1, 2, 3], then the server closed it with 1000 'the quiz is over'
viewer C received versions [0, 1, 2, 3], then the server closed it with 1000 'the quiz is over'
every handler ran on the main thread: True
threads in this process while serving: 1
now this program's own code: a ticker that counts every 0.05 s, beside a handler that waits 0.5 s
while the handler waited with await asyncio.sleep(0.5), the ticker ran 9 times
while the handler waited with time.sleep(0.5), the ticker ran 0 times

Drei Zuschauer wurden gleichzeitig bedient, in einem Thread, von deiner Coroutine.

Das Run sagt, dass jeder Handler im Hauptthread lief. Wie wurden drei Zuschauer gleichzeitig bedient?

Klicke dann auf Submit. Die nächste Lektion zeigt den Rand dieses Vorgeschmacks: was ein echter Kurs über asyncio hinzufügen würde.

Aufgabe

Schreibe in async_scoreboard.py viewer_handler(connection, next_snapshot) als -:

  1. Warte in einer next_snapshot() ab. Es liefert zuerst den Beitritts-Snapshot, dann jeden neueren Snapshot und None, wenn das Quiz vorbei ist.

  2. Liefert es None, warte connection.close(1000, "the quiz is over") ab (den Text des Grundes wählst du) und kehre zurück.

  3. Warte andernfalls connection.send(scoreboard_messages.encode_message(snapshot)) ab und frag dann erneut.

  4. Setze die Schleife in ein try, dessen except ConnectionClosed still zurückkehrt. Rufe close nach ConnectionClosed nicht auf: Diese Verbindung ist schon beendet.

Dein Handler wartet nur next_snapshot(), connection.send(...) und connection.close(...) ab. Submit führt ihn Schritt für Schritt ohne aus, also wird das Abwarten von irgendetwas anderem, das eine braucht (zum Beispiel asyncio.sleep(0.1)), als Fehler gemeldet. Warte nicht mit time.sleep, threading oder einem Event.wait(...): In einer Coroutine blockieren diese den einzigen Thread der Loop. label und two_labels aus Lektion 2 werden nicht geprüft.

Klicke auf Run: Der Abschnitt mit dem Stellvertreter gibt sent version 0 bis sent version 2 und closed 1000 the quiz is over aus, und pl_ws.py stellt deinen Handler drei Zuschauern bereit.

Klicke dann auf Submit. Die Prüfung gibt deinem Handler Stellvertreter und Snapshots, die du nicht gesehen hast, mit Versionen, die Nummern überspringen, und einem Teamnamen mit Akzent. Jeder Snapshot muss gesendet werden, in der richtigen Reihenfolge, als -Text dieses Snapshots, jeder nach dem next_snapshot(), das ihn geliefert hat; next_snapshot() muss nach dem letzten noch einmal abgewartet werden, und dann muss die Verbindung einmal geschlossen werden, mit 1000, ohne dass nach dem Schließen etwas gesendet wird. Wenn ein Senden ConnectionClosedError auslöst, muss der Handler still enden, ohne nach einem weiteren Snapshot zu fragen, und wenn das erste next_snapshot() None liefert, muss er mit 1000 schließen und nichts senden. Pythons Warnung “was never awaited” zählt als Fehler. Eine while True-Schleife oder eine Schleife mit einer Markierung, oder break nach dem Schließen, ein abgewartetes asyncio.sleep(0) und ein über einen Namen abgewartetes Senden sind alle in Ordnung.