Trage clients en verloren verbindingen · oefening
Laat elke kijker zijn eigen sends doen
Je hebt een outbox die alleen de nieuwste snapshot bewaart en wekt wie erop wacht, en geen kijker gebruikt er een. broadcast stuurt nog steeds naar elke kijker terwijl apply_score de lock van het bord vasthoudt, dus de scorebijhouder wacht nog steeds op de traagste kijker. Deze les verplaatst elke send naar de eigen thread van de kijker. De bibliotheek geeft elke verbinding al een thread, die viewer_handler draait, en tot nu toe hield die thread alleen de close in de gaten.
De nieuwe vorm
Vier veranderingen, en het register krijgt een andere betekenis:
board["viewers"]koppelt deconnection.idvan elke kijker aan zijn outbox, niet meer aan zijn verbinding;add_viewer(board, connection)maakt een outbox, en biedt die binnen één houding van de lock van het bord de huidige snapshot aan en registreert hem; daarna geeft hij de outbox terug;broadcast(board, snapshot)biedt de snapshot aan elke outbox in het register aan en stuurt niets;viewer_handlerdraait een: wachten op de outbox, sturen wat die bevat, en naar de verbinding kijken als er niets kwam.
Alleen de eigen thread van een kijker stuurt ooit naar die kijker. De afbeelding toont twee kijkers, A en S, op het moment dat de scorebijhouder versie 3 maakt, terwijl S nog bezig is met een trage send van versie 1. In de afbeelding van les 1 wachtte de scorebijhouder op S; vergelijk ze. Volg elke pijl naar beneden: welke thread doet elke send, en wat wacht er in de outbox van S?

In woorden: de thread van de scorebijhouder legt de nieuwe snapshot alleen in de outbox met één plek van elke kijker, wat geen tijd kost, en antwoordt. De thread van elke kijker wordt wakker, haalt de nieuwste snapshot uit zijn eigen outbox en stuurt hem. Terwijl de trage send van S loopt, worden nieuwere offers in de plek van S samengevoegd: die bevat één snapshot, de nieuwste. In de afbeelding stuurt de thread van S nog versie 1, en heeft versie 3 versie 2 vervangen in de outbox van S, dus S slaat versie 2 over en geen andere thread wacht erop.
add_viewer houdt de regel uit hoofdstuk 7 over binnenkomen: lees de snapshot, bied hem aan en registreer de outbox binnen één houding van de lock van het bord. Lees je de snapshot voordat je de lock neemt, of bied je hem aan nadat je de lock hebt vrijgegeven, dan kan er een score tussendoor glippen. Dan overschrijft een oudere snapshot bij binnenkomst de nieuwere die broadcast net aanbood, en hoort de kijker nooit over die score.
Volgorde door de opbouw
Versies bereiken een kijker op volgorde zonder extra moeite, omdat één thread naar hem stuurt, en elke snapshot die die thread stuurt de nieuwste is die hij vond. De volgende die hij ophaalt, kan alleen nieuwer zijn. De snapshot bij binnenkomst kan zelf vervangen worden voordat de thread hem stuurt. Dat is prima: wat de kijker als eerste ontvangt, is nog steeds een heel bord.
Updates 1 en 2 komen aan voordat de thread van een nieuwe kijker iets stuurt. Wat ontvangt de kijker als eerste?
Een thread die moet kijken
Dit is de lus die binnen de try van viewer_handler komt, na 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 is 0,5 s. Als er een snapshot komt, wekt het event de thread meteen en stuurt hij. Als er een halve seconde voorbijgaat zonder iets, roept de thread notice_close(connection) aan, wat connection.recv(timeout=0) binnen try is, met except TimeoutError: pass. Een time-out van 0 controleert zonder te wachten. Als de verbinding open en stil is, werpt recv meteen TimeoutError op en keert notice_close terug. Als er een bericht van de kijker wacht, geeft recv het terug en wordt het weggegooid, één per stille controle. Als de verbinding gesloten is, werpt recv ConnectionClosed op, wat de lus beëindigt via de except ConnectionClosed: die je al hebt, en het finally-blok verwijdert de kijker.
Waarom zou je überhaupt kijken? Een thread die alleen op zijn event wacht, hoort nooit dat zijn verbinding gesloten is. Erger nog: shutdown() van de bibliotheek sluit elke open verbinding en wacht daarna, zonder tijdslimiet, totdat elke handler is teruggekeerd. Een lokale test met een handler die zijn verbinding nooit meer aanraakte, vond shutdown() na 3 s nog steeds wachtend; die zou nooit terugkeren. Met de stille controle merkt de handler van een gesloten kijker het binnen een halve seconde en keert hij terug. Die halve seconde is de enige vertraging die de lus toevoegt, en alleen voor het opmerken van een close; een update wekt de thread meteen.
Waarom roept de lus van de kijker notice_close aan als wait_and_take None teruggeeft?
Waarom de kopie nu ertoe doet
Sinds hoofdstuk 7 zet snapshot_message board["scores"].copy() in elke snapshot. Tot nu toe maakte de kopie weinig verschil, omdat v1 elke snapshot verstuurde voordat het bord weer kon veranderen. Nu kan een snapshot in een outbox wachten terwijl apply_score het bord blijft veranderen. Zonder de kopie zou de "scores" van de wachtende snapshot de eigen
snapshot_message stopt met het kopiëren van scores. Wat kan een kijker toegestuurd krijgen?
Je opdracht
In scoreboard_server.py:
schrijf
notice_close(connection):connection.recv(timeout=0)binnentry, enexcept TimeoutError: pass;herschrijf
add_viewerzodat hij een outbox maakt, die binnen éénwith board["lock"]:snapshot_message(board)aanbiedt en hem registreert onderconnection.id, en hem teruggeeft;herschrijf
broadcastzodat hij de snapshot aanbiedt aan elke outbox inboard["viewers"];herschrijf
viewer_handler: houd zijntry,except ConnectionClosed: passenfinally: remove_viewer(board, connection), en zet binnen detryoutbox = add_viewer(board, connection)en de lus hierboven;verwijder
watch_until_closed, die niets meer aanroept.
remove_viewer blijft zoals hij is.
Uitvoeren
Klik op Run. De sectie van les 3 geeft viewer_handler een stand-in-kijker, en start tijdens de eerste send van de server naar die kijker een thread van de scorebijhouder die drie keer scoort, waarop met een time-out van 0,5 s wordt gewacht. Met v1 rondde de scorebijhouder in die tijd 0 van de 3 updates af, omdat de eerste send de lock van het bord vasthield, en kreeg de kijker de versies [0, 1, 2, 3] toegestuurd. Een afgemaakte versie, op de site uitgevoerd, drukte af:
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]
De scorebijhouder wacht niet meer op de kijker, en de kijker sloeg versie 1 en 2 over, omdat versie 3 ze verving terwijl hij bezig was. De run duurt nu ongeveer een halve seconde langer, de stille controles van de stand-in-kijker. Klik daarna op Submit.
Les 4 richt zich op een kijker die niet traag is, maar weg.
Opdracht
In scoreboard_server.py:
Schrijf
notice_close(connection): roepconnection.recv(timeout=0)aan binnentry, metexcept TimeoutError: pass. DeConnectionClosedvan een gesloten verbinding moet naar buiten kunnen.Herschrijf
add_viewer(board, connection): maak een outbox metmake_outbox(); bied hem binnen éénwith board["lock"]:snapshot_message(board)aan en sla hem op alsboard["viewers"][connection.id]; geef de outbox terug. Hij stuurt niets.Herschrijf
broadcast(board, snapshot): biedsnapshotaan aan elke outbox inboard["viewers"]. Hij stuurt niets, en hij neemt de lock van het bord niet, dieapply_scoreal vasthoudt.Herschrijf
viewer_handler(connection, board): houd detry,except ConnectionClosed: passenfinally: remove_viewer(board, connection). Roep binnen detryoutbox = add_viewer(board, connection)aan, en ga daarna eindeloos rond in een: snapshot = wait_and_take(outbox, CHECK_EVERY); als datNoneis, roep jenotice_close(connection)aan; anders stuur jescoreboard_messages.encode_message(snapshot).Verwijder
watch_until_closed. Laatremove_viewerzoals hij is.
De stille controle is verplicht: een kijkerthread die alleen op zijn event wacht, hoort nooit dat zijn verbinding gesloten is, en shutdown() van de server wacht totdat elke handler is teruggekeerd.
Klik op Run: de sectie van les 3 drukt the scorekeeper finished 3 of 3 updates en the viewer was sent versions [0, 3] af.
Klik daarna op Submit. De controle vervangt de threading door een stand-in, dus er start geen thread en er wacht niets. Ze draait je viewer_handler op een stand-in-kijker die binnenkomt, twee stille controles meemaakt en sluit, één keer voor elk moment waarop een andere thread zou kunnen draaien (elke lock die wordt genomen of vrijgegeven, elke send, elke stille controle), met één score, en daarna drie, die op dat moment aankomen. Elke keer moet het eerste bericht van de kijker een snapshot zijn, moeten zijn versies alleen stijgen, moeten de scores van elke snapshot bij zijn versie passen, moet de nieuwste versie op het moment van zijn laatste stille controle hem bereiken, mag er niets worden verstuurd terwijl een lock wordt vastgehouden, en moet het register aan het eind leeg zijn. Ze controleert ook notice_close los, broadcast met drie geregistreerde kijkers, en vier eindes: een close door RuntimeError uit recv, en een kijker van wie nooit meer iets aankomt. De outbox binnen of buiten de lock van het bord maken is prima, en acquire() en release() in plaats van with ook.