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
Ein Platz
Eine Outbox ist ein
"snapshot": der Snapshot, der darauf wartet, gesendet zu werden, oderNone, wenn nichts wartet;"lock": ein eigenesthreading.Lock, für die Momente, in denen sich der Platz ändert;"event": einthreading.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:
| Schritt | Platz danach | Event danach | take gibt zurück |
|---|---|---|---|
offer Version 1 | Version 1 | gesetzt | |
offer Version 2 | Version 2 | gesetzt | |
take | leer | gelöscht | Version 2 |
offer Version 3 | Version 3 | gesetzt | |
take | leer | gelöscht | Version 3 |
Version 1 wurde nie genommen: Version 2 hat sie ersetzt, während sie wartete. Nur den neuesten 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:
make_outbox(): ein neues Dictionary mit"snapshot": None, einem Lock austhreading.Lock()und einem Event austhreading.Event();offer(outbox, snapshot): Speichere innerhalb vonwith outbox["lock"]:den Snapshot und setze das Event;take(outbox): Lies innerhalb vonwith outbox["lock"]:den Platz, setze ihn aufNone, lösche das Event und gib zurück, was du gelesen hast;wait_and_take(outbox, timeout): Warte mittimeout=timeoutauf das Event und gib danntake(outbox)zurück.
Rufe threading.Lock() und threading.Event() über das 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
make_outbox(): Gib{"snapshot": None, "lock": threading.Lock(), "event": threading.Event()}zurück, bei jedem Aufruf mit einem neuen Lock und einem neuen Event.offer(outbox, snapshot): Lege innerhalb vonwith outbox["lock"]:snapshotinoutbox["snapshot"], wobei du alles ersetzt, was wartet, und setzeoutbox["event"].take(outbox): Lies innerhalb vonwith outbox["lock"]:outbox["snapshot"], setze es aufNone, lösche das Event und gib zurück, was du gelesen hast (None, wenn nichts gewartet hat).wait_and_take(outbox, timeout): Rufeoutbox["event"].wait(timeout=timeout)außerhalb des Locks auf und gib danntake(outbox)zurück.
Schreibe threading.Lock() und threading.Event() über das 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 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.