Eén server, veel clients · oefening
Houd een register van kijkers bij
Na les 2 ontvangt een kijker zijn snapshot, en daarna vergeet de server hem. Als er een score binnenkomt, weet niets welke kijkers het moeten horen. Deze les houdt een register van verbonden kijkers bij, dicht een race op het moment dat een kijker binnenkomt, en zorgt ervoor dat elke kijker het register verlaat, hoe zijn verbinding ook eindigt.
Een register met id als sleutel
Het register is board["viewers"], een connection.id, een send kan aanroepen. De id is de sleutel in plaats van de verbinding, omdat een stand-in-verbinding die met types.SimpleNamespace is gebouwd, het soort dat de demo en de tests gebruiken, zelf geen
add_viewer(board, connection) slaat de verbinding op en stuurt haar snapshot bij binnenkomst. remove_viewer(board, connection) haalt haar er weer uit. Tussen die twee aanroepen zit de handler van de kijker in watch_until_closed, en elke andere thread die in het register kijkt, vindt haar daar. Kijken betekent door de dict lopen, bijvoorbeeld met .items(), dat elke id samen met zijn verbinding geeft:
for viewer_id, viewer in board["viewers"].items():
print(viewer_id, viewer.request.path)
Het register staat op het bord, naast de scores, om dezelfde reden als de scores: elke
De race bij de deur
Meerdere threads gebruiken het bord tegelijk: één per kijker, en die van de scorebijhouder. Binnenkomen bestaat uit drie stappen: het bord in een snapshot lezen, registreren en de snapshot sturen. De afbeelding volgt de thread van de kijker links en die van de scorebijhouder rechts, met de tijd van boven naar beneden; de bovenste helft heeft geen lock. Zoek het moment waarop versie 6 de deur uitgaat: is de kijker dan al geregistreerd?

In woorden: de thread van de kijker leest versie 5; de thread van de scorebijhouder verwerkt een score, wat versie 6 oplevert, en stuurt die naar elke geregistreerde kijker; pas daarna registreert de kijker zich en stuurt zijn snapshot van versie 5. Hij toont versie 5 en hoort niets meer tot de volgende score.
Schuif de score één stap later, en er gaat iets anders mis: de kijker leest versie 5 en registreert zich, versie 6 wordt naar hem gebroadcast, en daarna wordt de snapshot van versie 5 die hij eerder las verstuurd, na de nieuwere.
Het antwoord kwam je tegen in Networks with Python: een lock rond een lees- en een schrijfactie, zodat geen andere thread ertussen kan handelen. Hier gaan het lezen, het registreren en het sturen allemaal in één with board["lock"]:-blok in add_viewer. Dat is de onderste helft van de afbeelding: de scorebijhouder wacht op de lock, en verwerkt en verstuurt daarna versie 6. De thread van de scorebijhouder neemt dezelfde lock om een score te verwerken (les 4), dus een score gebeurt ofwel helemaal vóórdat de kijker binnenkomt, en dan bevat de snapshot hem al, ofwel helemaal erna, en dan is de kijker geregistreerd om hem te horen. remove_viewer neemt de lock ook.
Een kijker leest de snapshot binnen de lock, geeft die vrij, en registreert zich daarna binnen een tweede houding. Wat kan er misgaan?
finally: opruimen dat altijd draait
Een kijker moet het register verlaten, hoe zijn handler ook eindigt: een nette close, een verbinding die wegviel, een send die mislukte, of een bug die iets onverwachts opwierp. Python heeft daar precies een blok voor. Deze cursus heeft het nu nodig, dus hier is het; Python Fundamentals II behandelt het volledig.
Een try-statement kan eindigen met finally:. Dat blok draait op elke weg uit de try: na de laatste regel, na een finally-blok gewoon verdergaat.
def show(how):
try:
if how == "return":
return "returned"
if how == "raise":
raise RuntimeError("broken")
print("reached the end")
finally:
print("finally ran")
Lees het met elk van drie show("end") drukt beide regels af. show("return") drukt finally ran af en keert dan terug. show("raise") drukt finally ran af, en dan bereikt de RuntimeError de aanroeper.
Wat drukt dit af?
def step():
try:
print("try")
return
finally:
print("finally")
step()
print("after")
Voer het uit om je antwoord te controleren:
De handler van de kijker wordt:
def viewer_handler(connection, board):
try:
add_viewer(board, connection)
watch_until_closed(connection)
except ConnectionClosed:
pass
finally:
remove_viewer(board, connection)
add_viewer staat binnen de try omdat zijn eigen send de verbinding al weg kan vinden; dan vangt except ConnectionClosed: dat op, en het finally-blok verwijdert de kijker toch. Die clausule houdt ook een normaal einde uit het log van de bibliotheek. Elke andere exception wordt niet gevangen: die gaat door finally, dat opruimt, en bereikt de bibliotheek, die hem logt en de verbinding sluit met 1011. Dat wil je bij een echte bug. Als je hier elke exception zou vangen, zou je hem verbergen, en zou de kijker vertrekken zonder dat iemand te weten kwam waarom.
Er volgt niets na het try-statement in de handler. Zodra het finally-blok heeft gedraaid, keert de handler terug, en rondt de bibliotheek het sluiten van de verbinding af.
remove_viewer gebruikt board["viewers"].pop(connection.id, None). Het tweede argument is wat pop teruggeeft als de sleutel ontbreekt, en daarmee is een ontbrekende sleutel geen fout. De sleutel kan ontbreken: vanaf les 4 verwijdert een broadcast een kijker wiens send mislukte, en de handler van die kijker bereikt daarna zijn eigen finally.
add_viewer werpt ConnectionClosedError op omdat de kijker meteen vertrok. Welke regels draaien?
Een lock vasthouden tijdens het sturen
add_viewer stuurt de snapshot bij binnenkomst terwijl hij de lock vasthoudt. Dat houdt de volgorde eenvoudig en zeker: niets kan het bord veranderen totdat de snapshot onderweg is. De prijs is dat elke andere thread op die send wacht, en hoofdstuk 8 meet wat dat kost en betaalt het.
Houd de lock alleen vast binnen add_viewer en remove_viewer, nooit rond watch_until_closed: een handler die hem vasthield terwijl hij op de kijker wachtte, zou elke score tegenhouden totdat die kijker vertrok.
Uitvoeren
Klik op Run. Een afgemaakte versie, op de site uitgevoerd, drukte af:
Lesson 3: the registry
while A is connected, the registry holds: ['A']
after A's handler returned, the registry holds: []
after B failed at its first send, the registry holds: []
De sectie geeft kijker A een script waarin één aanroep van recv in het register kijkt terwijl A verbonden is, en daarna de verbinding met een nette close beëindigt. Kijker A staat in het register zolang hij verbonden is, en is weg zodra zijn handler terugkeert. De send bij binnenkomst van kijker B mislukt; de except-clausule vangt dat en finally verwijdert B, dus de foutregel uit les 2 is weg. Klik daarna op Submit.
Het register kent nu elke kijker. De volgende les verandert het bord en stuurt elke verandering naar ze allemaal.
Opdracht
Schrijf in scoreboard_server.py twee
add_viewer(board, connection): sla binnen éénwith board["lock"]:-blok de verbinding op metboard["viewers"][connection.id] = connectionen stuur haarscoreboard_messages.encode_message(snapshot_message(board)).remove_viewer(board, connection): roep binnenwith board["lock"]:board["viewers"].pop(connection.id, None)aan.viewer_handler(connection, board): roep in eentry-blokadd_viewer(board, connection)en daarnawatch_until_closed(connection)aan; voegexcept ConnectionClosed:metpasstoe; en roepremove_viewer(board, connection)aan in eenfinally-blok.
Houd import threading en gebruik board["lock"]; maak geen eigen lock.
Klik op Run en lees de sectie van les 3, en klik daarna op Submit. De controle houdt de lock van het bord in de gaten in plaats van threads te draaien. Ze laat een kijker één keer binnenkomen om de momenten te tellen waarop een andere thread zou kunnen handelen (elke keer dat de lock wordt genomen of vrijgegeven, en elke send), en daarna nog een keer per moment, met een score van de thread van de scorebijhouder die precies daar wordt verwerkt. Elke keer moet het eerste bericht van de kijker een snapshot zijn, moeten zijn versies alleen omhooggaan, moeten de punten van elke snapshot bij zijn versie passen, moet de nieuwste versie van vóór het vertrek van de kijker hem hebben bereikt, en moet het register aan het eind leeg zijn. De send bij binnenkomst moet gebeuren terwijl de lock wordt vastgehouden, en geen enkele recv terwijl hij wordt vastgehouden. Daarna laat ze een kijker op zes manieren eindigen: een nette close, een verbinding zonder close-RuntimeError uit recv, een recv die eeuwig zou wachten, en een kijker die een broadcast al had verwijderd. Elke manier moet het register leeg en de lock vrij laten, en een gesloten verbinding mag niet uit de handler ontsnappen. Een kijker die vertrekt, mag geen andere verwijderen. acquire() en release() in een eigen try en finally zijn net zo goed als with.