0%

Langsame Clients und verlorene Verbindungen · Übung

Nur den neuesten Snapshot behalten

Lektion 1 hat sich auf eine Strategie festgelegt: Für jeden Zuschauer wird nur der neueste Snapshot behalten. Ein Snapshot enthält die ganze Tafel, also kann der ältere weg, wenn ein neuerer ankommt, bevor der ältere gesendet wurde. Diese Lektion baut den Ort, an dem dieser Snapshot wartet, eine Outbox, mit vier kleinen . Noch verwendet kein Zuschauer eine Outbox; Lektion 3 gibt jedem eine.

Ein Platz

Eine Outbox ist ein mit drei Schlüsseln:

  • "snapshot": der Snapshot, der darauf wartet, gesendet zu werden, oder None, wenn nichts wartet;

  • "lock": ein eigenes threading.Lock, für die Momente, in denen sich der Platz ändert;

  • "event": ein threading.Event, das der nächste Abschnitt vorstellt und das genau dann gesetzt ist, wenn ein Snapshot wartet.

offer(outbox, snapshot) legt einen Snapshot in den Platz und ersetzt, was dort war, und take(outbox) gibt zurück, was wartet, und lässt den Platz leer zurück. Hier ist eine Outbox, die drei Angebote und zwei Entnahmen durchläuft:

SchrittPlatz danachEvent danachtake gibt zurück
offer Version 1Version 1gesetzt
offer Version 2Version 2gesetzt
takeleergelöschtVersion 2
offer Version 3Version 3gesetzt
takeleergelöschtVersion 3

Version 1 wurde nie genommen: Version 2 hat sie ersetzt, während sie wartete. Nur den neuesten zu behalten, sodass ein neuerer einen älteren ersetzt, der nie verwendet wurde, nennt man Zusammenfassen (Coalescing). Vergleiche das mit einer Warteschlange, einer , an die offer anhängt und von der take vorne liest. Eine Warteschlange würde jede Version der Reihe nach herausgeben, und während ihr Zuschauer langsam ist, würde sie um einen Snapshot pro Wertung wachsen. Ein Platz enthält einen einzigen Snapshot, egal wie weit sein Zuschauer zurückfällt, also kostet ein langsamer Zuschauer den Server genauso viel Speicher wie ein schneller.

Die Versionen 4, 5 und 6 werden angeboten, bevor der Thread des Zuschauers etwas nimmt. Was gibt take zurück?

Ein Event weckt den Sender

Der Thread, der einem Zuschauer seine Snapshots senden wird, muss schlafen, solange der Platz leer ist, und aufwachen, sobald etwas ankommt. threading.Event erledigt das. Ein Event ist eine Markierung, die gelöscht beginnt: set() setzt sie, clear() löscht sie, is_set() meldet sie, und wait(timeout=...) blockiert, bis die Markierung gesetzt ist oder das Timeout abläuft. wait gibt True zurück, wenn das Event gesetzt wurde, und False, wenn zuerst die Zeit abgelaufen ist. In CPython kehrt ein wait mit Timeout zurück, sobald ein anderer Thread set() aufruft; es schläft nicht das ganze Timeout ab. Genau darauf verlässt sich die Outbox.

In Networks with Python hat rendezvous in fakes.py ein Event verwendet, und jene Lektion sagte, dass du selbst keins brauchen würdest. Jetzt brauchst du eins. Hier wartet ein Thread, und ein anderer setzt das Event:

import threading

ready = threading.Event()


def sender():
    if ready.wait(timeout=2.0):
        print("woken by set()")
    else:
        print("two seconds passed and nothing came")


thread = threading.Thread(target=sender)
thread.start()
ready.set()
thread.join(timeout=3.0)

sender gibt sofort woken by set() aus. Entferne die Zeile ready.set(), und es gibt nach zwei Sekunden die andere Zeile aus.

Eine halbe Sekunde lang setzt nichts ein Event. Was gibt event.wait(timeout=0.5) zurück, und wann?

wait_and_take(outbox, timeout) ist die Hälfte des Senders: Warte mit dem übergebenen Timeout auf das Event der Outbox und gib dann take(outbox) zurück. Wenn nichts kam, ist der Platz leer, und take gibt None zurück, also erfährt der Aufrufer, dass die Zeit abgelaufen ist. Das Timeout ist wichtig, obwohl das Event den Thread früher weckt: Lektion 3 nutzt die stillen Momente, um nach der Verbindung des Zuschauers zu schauen.

Speichern und Setzen zusammen

Das Event muss genau dann gesetzt sein, wenn ein Snapshot wartet. Wäre es gelöscht, während ein Snapshot wartet, könnte der Sender eine Wertung verschlafen, die er senden sollte. Deshalb speichert offer den Snapshot und setzt das Event in einem einzigen with outbox["lock"]:-Block, und take liest den Platz, leert ihn und löscht das Event ebenfalls in einem einzigen Block. Dann kann kein anderer Thread sehen, dass Platz und Event sich widersprechen.

Nimm ein take, das das Event erst nach dem Ende seines with-Blocks löscht. Zwischen dem Ende des Blocks und dem clear() kann ein anderer Thread ein offer ausführen: Er speichert einen neuen Snapshot und setzt das Event. Dann löscht das verspätete clear() es wieder. Jetzt wartet ein Snapshot unter einem gelöschten Event, und der Thread des Zuschauers schläft vielleicht, statt ihn zu senden.

take löscht das Event nach dem Ende seines with-Blocks. Was kann schiefgehen?

wait_and_take wartet außerhalb des Locks. Während es wartet, muss ein offer das Lock nehmen und den Platz füllen können.

Zwei Locks, eine Reihenfolge

Die Tafel hat ihr Lock, und jede Outbox hat jetzt ihr eigenes. Zwei Threads, die zwei Locks in entgegengesetzter Reihenfolge nehmen, können ewig aufeinander warten. Hier kann das nicht passieren, weil die Reihenfolge immer dieselbe ist. offer wird laufen, während apply_score das Lock der Tafel hält: zuerst das Lock der Tafel, dann ein Outbox-Lock. Der Thread eines Zuschauers nimmt das Lock der Tafel nur zum Beitreten und zum Gehen, niemals während er das Lock seiner Outbox hält. Kein Thread hält jemals ein Outbox-Lock und verlangt dann das Lock der Tafel.

Deine Aufgabe

Schreibe in scoreboard_server.py die vier Funktionen von Lektion 2:

  1. make_outbox(): ein neues Dictionary mit "snapshot": None, einem Lock aus threading.Lock() und einem Event aus threading.Event();

  2. offer(outbox, snapshot): Speichere innerhalb von with outbox["lock"]: den Snapshot und setze das Event;

  3. take(outbox): Lies innerhalb von with outbox["lock"]: den Platz, setze ihn auf None, lösche das Event und gib zurück, was du gelesen hast;

  4. wait_and_take(outbox, timeout): Warte mit timeout=timeout auf das Event und gib dann take(outbox) zurück.

Rufe threading.Lock() und threading.Event() über das auf, innerhalb von make_outbox. Die Prüfung ersetzt das Modul threading durch einen Stellvertreter, der jedes Lock und jedes Event aufzeichnet, und sie kann keins sehen, das beim Importieren der Datei erstellt oder mit from threading import importiert wurde.

Ausführen

Klicke auf Run. Die beiden Abschnitte zu Lektion 2 in scoreboard_demo.py verwenden jetzt deine Funktionen. Der erste bietet einer Outbox drei Snapshots an und nimmt zweimal; der zweite startet einen Thread in wait_and_take(outbox, 2.0) und bietet dann Version 9 an. Eine fertige Version hat auf der Seite ausgegeben:

Lesson 2: an outbox keeps only the newest snapshot
    after three offers, the event is set: True
    take: {'type': 'snapshot', 'version': 3, 'scores': {}}
    take again: None and the event is set: False
Lesson 2: a waiting thread wakes up
    the waiting thread took version 9

Der wartende Thread hat Version 9 genommen, ohne seine zwei Sekunden abzuschlafen: Sobald das Event gesetzt war, ist wait zurückgekehrt. Klicke dann auf Submit.

Lektion 3 gibt jedem Zuschauer seine eigene Outbox und verlegt jedes Senden in den eigenen Thread dieses Zuschauers.

Aufgabe

Schreibe in scoreboard_server.py die vier von Lektion 2:

  1. make_outbox(): Gib {"snapshot": None, "lock": threading.Lock(), "event": threading.Event()} zurück, bei jedem Aufruf mit einem neuen Lock und einem neuen Event.

  2. offer(outbox, snapshot): Lege innerhalb von with outbox["lock"]: snapshot in outbox["snapshot"], wobei du alles ersetzt, was wartet, und setze outbox["event"].

  3. take(outbox): Lies innerhalb von with outbox["lock"]: outbox["snapshot"], setze es auf None, lösche das Event und gib zurück, was du gelesen hast (None, wenn nichts gewartet hat).

  4. wait_and_take(outbox, timeout): Rufe outbox["event"].wait(timeout=timeout) außerhalb des Locks auf und gib dann take(outbox) zurück.

Schreibe threading.Lock() und threading.Event() über das , innerhalb von make_outbox: Die Prüfung ersetzt das threading-Modul von scoreboard_server durch einen Stellvertreter, der jedes Lock und jedes Event aufzeichnet, und sie lehnt ein Lock oder Event ab, das beim Importieren der Datei erstellt oder mit from threading import hereingeholt wurde.

Klicke auf Run: Die beiden Abschnitte zu Lektion 2 geben take: {'type': 'snapshot', 'version': 3, 'scores': {}}, take again: None and the event is set: False und the waiting thread took version 9 aus.

Klicke dann auf Submit. Die Prüfung bietet einer neuen Outbox Snapshots an, die du nicht gesehen hast (Versionen 17, 18 und 19), und erwartet, dass take Version 19 zurückgibt, ein zweites take None zurückgibt, das Event nach jedem Angebot gesetzt und nach jeder Entnahme gelöscht ist und jedes Setzen und Löschen passiert, während das Lock dieser Outbox gehalten wird. Sie bietet tausend Snapshots an und erwartet einen im Platz, nie eine . Sie ruft wait_and_take(outbox, 0.75) auf und erwartet, dass 0.75 bei wait ankommt, dass der wartende Snapshot zurückkommt und dass None kommt, wenn nichts wartet. acquire() und release() statt with sind in Ordnung, und ebenso ein wait_and_take, das None zurückgibt, ohne zu nehmen, wenn wait False zurückgibt.