Langsame Clients und verlorene Verbindungen · Übung
Jeder Zuschauer sendet selbst
Du hast eine Outbox, die nur den neuesten Snapshot behält und weckt, wer auf sie wartet, und kein Zuschauer verwendet eine. broadcast sendet weiterhin an jeden Zuschauer, während apply_score das Lock der Tafel hält, also wartet der Punktezähler weiterhin auf den langsamsten Zuschauer. Diese Lektion verlegt jedes Senden in den eigenen Thread des Zuschauers. Die Bibliothek gibt jeder Verbindung schon einen Thread, den, der viewer_handler ausführt, und bisher hat dieser Thread nur auf das Schließen geachtet.
Die neue Form
Vier Änderungen, und das Register ändert seine Bedeutung:
board["viewers"]ordnet dieconnection.idjedes Zuschauers seiner Outbox zu, nicht mehr seiner Verbindung;add_viewer(board, connection)erstellt eine Outbox, bietet ihr in einem einzigen Halten des Tafel-Locks den aktuellen Snapshot an und registriert sie; dann gibt es die Outbox zurück;broadcast(board, snapshot)bietet den Snapshot jeder Outbox im Register an und sendet nichts;viewer_handlerläuft in einer: auf die Outbox warten, senden, was sie enthält, und nach der Verbindung schauen, wenn nichts kam.
Nur der eigene Thread eines Zuschauers sendet jemals an diesen Zuschauer. Das Bild zeigt zwei Zuschauer, A und S, in dem Moment, in dem der Punktezähler Version 3 erzeugt, während S noch mit einem langsamen Senden von Version 1 beschäftigt ist. Im Bild aus Lektion 1 hat der Punktezähler auf S gewartet; vergleiche. Folge jedem Pfeil nach unten: Welcher Thread übernimmt welches Senden, und was wartet in der Outbox von S?

In Worten: Der Thread des Punktezählers legt den neuen Snapshot nur in die Outbox mit einem Platz jedes Zuschauers, was keine Zeit kostet, und antwortet. Der Thread jedes Zuschauers wacht auf, nimmt den neuesten Snapshot aus seiner eigenen Outbox und sendet ihn. Während das langsame Senden von S läuft, werden neuere Angebote im Platz von S zusammengefasst: Er enthält einen Snapshot, den neuesten. Im Bild sendet der Thread von S noch Version 1, und Version 3 hat Version 2 in der Outbox von S ersetzt, also überspringt S Version 2, und kein anderer Thread wartet auf ihn.
add_viewer behält die Regel aus Kapitel 7 für den Beitritt: den Snapshot lesen, ihn anbieten und die Outbox registrieren, alles in einem einzigen Halten des Tafel-Locks. Liest du den Snapshot, bevor du das Lock nimmst, oder bietest du ihn an, nachdem du das Lock freigegeben hast, kann eine Wertung dazwischenrutschen. Dann überschreibt ein älterer Beitritts-Snapshot den neueren, den broadcast gerade angeboten hat, und der Zuschauer hört nie von dieser Wertung.
Reihenfolge durch Konstruktion
Versionen erreichen einen Zuschauer ohne zusätzliche Sorgfalt in der richtigen Reihenfolge, denn ein einziger Thread sendet an ihn, und jeder Snapshot, den dieser Thread sendet, ist der neueste, den er gefunden hat. Der nächste, den er nimmt, kann nur neuer sein. Der Beitritts-Snapshot selbst kann ersetzt werden, bevor der Thread ihn sendet. Das ist in Ordnung: Was der Zuschauer zuerst empfängt, ist trotzdem eine ganze Tafel.
Die Updates 1 und 2 kommen an, bevor der Thread eines neuen Zuschauers irgendetwas sendet. Was empfängt der Zuschauer zuerst?
Ein Thread, der nachschauen muss
Hier ist die Schleife, die in das try von viewer_handler kommt, nach outbox = add_viewer(board, connection):
while True:
snapshot = wait_and_take(outbox, CHECK_EVERY)
if snapshot is None:
notice_close(connection)
else:
connection.send(scoreboard_messages.encode_message(snapshot))
CHECK_EVERY beträgt 0,5 s. Wenn ein Snapshot kommt, weckt das Event den Thread sofort, und er sendet. Wenn eine halbe Sekunde ohne etwas vergeht, ruft der Thread notice_close(connection) auf, also connection.recv(timeout=0) innerhalb von try, mit except TimeoutError: pass. Ein Timeout von 0 prüft, ohne zu warten. Ist die Verbindung offen und still, löst recv sofort TimeoutError aus, und notice_close kehrt zurück. Wartet eine Nachricht des Zuschauers, gibt recv sie zurück, und sie wird verworfen, eine pro Leerlaufprüfung. Ist die Verbindung geschlossen, löst recv ConnectionClosed aus, was die Schleife über das except ConnectionClosed: beendet, das du schon hast, und der finally-Block entfernt den Zuschauer.
Warum überhaupt nachschauen? Ein Thread, der nur auf sein Event wartet, erfährt nie, dass seine Verbindung geschlossen wurde. Schlimmer noch: shutdown() der Bibliothek schließt jede offene Verbindung und wartet dann ohne Zeitlimit, bis jeder Handler zurückgekehrt ist. Ein lokaler Test mit einem Handler, der seine Verbindung nie wieder anfasste, fand shutdown() nach 3 s noch wartend vor; es wäre nie zurückgekehrt. Mit der Leerlaufprüfung bemerkt der Handler eines geschlossenen Zuschauers das innerhalb einer halben Sekunde und kehrt zurück. Diese halbe Sekunde ist die einzige Verzögerung, die die Schleife hinzufügt, und nur für das Bemerken eines Schließens; ein Update weckt den Thread sofort.
Warum ruft die Zuschauer-Schleife notice_close auf, wenn wait_and_take None zurückgibt?
Warum die Kopie jetzt wichtig ist
Seit Kapitel 7 legt snapshot_message board["scores"].copy() in jeden Snapshot. Bisher hat die Kopie wenig Unterschied gemacht, weil v1 jeden Snapshot gesendet hat, bevor sich die Tafel erneut ändern konnte. Jetzt kann ein Snapshot in einer Outbox warten, während apply_score die Tafel weiter ändert. Ohne die Kopie wäre "scores" des wartenden Snapshots das eigene
snapshot_message hört auf, scores zu kopieren. Was kann einem Zuschauer gesendet werden?
Deine Aufgabe
In scoreboard_server.py:
Schreibe
notice_close(connection):connection.recv(timeout=0)innerhalb vontryundexcept TimeoutError: pass;schreibe
add_viewerso um, dass es eine Outbox erstellt, ihrsnapshot_message(board)anbietet und sie innerhalb eines einzigenwith board["lock"]:unterconnection.idregistriert, und sie zurückgibt;schreibe
broadcastso um, dass es den Snapshot jeder Outbox inboard["viewers"]anbietet;schreibe
viewer_handlerum: Behalte seintry,except ConnectionClosed: passundfinally: remove_viewer(board, connection), und setze in dastryoutbox = add_viewer(board, connection)und die Schleife von oben;lösche
watch_until_closed, das nichts mehr aufruft.
remove_viewer bleibt, wie es ist.
Ausführen
Klicke auf Run. Der Abschnitt zu Lektion 3 gibt viewer_handler einen Stellvertreter-Zuschauer und startet während des ersten Sendens des Servers an ihn einen Punktezähler-Thread, der dreimal wertet und mit einem Timeout von 0,5 s abgewartet wird. Mit v1 hat der Punktezähler in dieser Zeit 0 von 3 Updates geschafft, weil das erste Senden das Lock der Tafel hielt, und dem Zuschauer wurden die Versionen [0, 1, 2, 3] gesendet. Eine fertige Version hat auf der Seite ausgegeben:
Lesson 3: a busy viewer and a scorekeeper
while the server was sending the viewer its first snapshot, the scorekeeper finished 3 of 3 updates
the viewer was sent versions [0, 3]
Der Punktezähler wartet nicht mehr auf den Zuschauer, und der Zuschauer hat die Versionen 1 und 2 übersprungen, weil Version 3 sie ersetzt hat, während er beschäftigt war. Der Lauf dauert jetzt etwa eine halbe Sekunde länger, wegen der Leerlaufprüfungen des Stellvertreter-Zuschauers. Klicke dann auf Submit.
Lektion 4 wendet sich einem Zuschauer zu, der nicht langsam, sondern weg ist.
Aufgabe
In scoreboard_server.py:
Schreibe
notice_close(connection): Rufeconnection.recv(timeout=0)innerhalb vontryauf, mitexcept TimeoutError: pass. DasConnectionClosedeiner geschlossenen Verbindung muss hinauskommen.Schreibe
add_viewer(board, connection)um: Erstelle eine Outbox mitmake_outbox(); biete ihr innerhalb eines einzigenwith board["lock"]:snapshot_message(board)an und speichere sie alsboard["viewers"][connection.id]; gib die Outbox zurück. Diesendet nichts. Schreibe
broadcast(board, snapshot)um: Bietesnapshotjeder Outbox inboard["viewers"]an. Die Funktion sendet nichts, und sie nimmt nicht das Lock der Tafel, dasapply_scoreschon hält.Schreibe
viewer_handler(connection, board)um: Behalte dastry,except ConnectionClosed: passundfinally: remove_viewer(board, connection). Rufe innerhalb destryoutbox = add_viewer(board, connection)auf und laufe dann endlos in einer: snapshot = wait_and_take(outbox, CHECK_EVERY); wenn esNoneist, rufenotice_close(connection)auf; andernfalls sendescoreboard_messages.encode_message(snapshot).Lösche
watch_until_closed. Lassremove_viewer, wie es ist.
Die Leerlaufprüfung ist Pflicht: Ein Zuschauer-Thread, der nur auf sein Event wartet, erfährt nie, dass seine Verbindung geschlossen wurde, und shutdown() des Servers wartet darauf, dass jeder Handler zurückkehrt.
Klicke auf Run: Der Abschnitt zu Lektion 3 gibt the scorekeeper finished 3 of 3 updates und the viewer was sent versions [0, 3] aus.
Klicke dann auf Submit. Die Prüfung ersetzt das threading durch einen Stellvertreter, also startet kein Thread, und nichts wartet. Sie führt deinen viewer_handler auf einem Stellvertreter-Zuschauer aus, der beitritt, zwei stille Leerlaufprüfungen erlebt und schließt, und zwar einmal für jeden Moment, in dem ein anderer Thread laufen könnte (jedes Nehmen oder Freigeben eines Locks, jedes Senden, jede Leerlaufprüfung), wobei in diesem Moment eine Wertung und dann drei ankommen. Jedes Mal muss die erste Nachricht des Zuschauers ein Snapshot sein, seine Versionen dürfen nur steigen, die Punktestände jedes Snapshots müssen zu seiner Version passen, die neueste Version zum Zeitpunkt seiner letzten Leerlaufprüfung muss ihn erreichen, nichts darf gesendet werden, während ein Lock gehalten wird, und das Register muss am Ende leer sein. Sie prüft außerdem notice_close für sich allein, broadcast mit drei registrierten Zuschauern und vier Enden: ein Schließen durch RuntimeError aus recv und ein Zuschauer, von dem nie wieder etwas ankommt. Die Outbox innerhalb oder außerhalb des Tafel-Locks zu erstellen ist in Ordnung, und ebenso acquire() und release() statt with.