0%

Trage clients en verloren verbindingen · oefening

Bewaar alleen de nieuwste snapshot

Les 1 koos een beleid: bewaar voor elke kijker alleen de nieuwste snapshot. Een snapshot bevat het hele bord, dus als er een nieuwere aankomt voordat de oudere is verstuurd, kan de oudere weg. Deze les bouwt de plek waar die snapshot wacht, een outbox, met vier kleine . Nog geen kijker gebruikt een outbox; les 3 geeft er elke kijker een.

Eén plek

Een outbox is een met drie sleutels:

  • "snapshot": de snapshot die wacht om verstuurd te worden, of None als er niets wacht;

  • "lock": een eigen threading.Lock, voor de momenten waarop de plek verandert;

  • "event": een threading.Event, die de volgende sectie introduceert, gezet precies zolang er een snapshot wacht.

offer(outbox, snapshot) zet een snapshot op de plek en vervangt wat daar stond, en take(outbox) geeft terug wat er wacht en laat de plek leeg achter. Hier gaat één outbox door drie offers en twee takes:

StapPlek daarnaEvent daarnatake geeft terug
offer versie 1versie 1gezet
offer versie 2versie 2gezet
takeleeggewistversie 2
offer versie 3versie 3gezet
takeleeggewistversie 3

Versie 1 werd nooit opgehaald: versie 2 verving haar terwijl ze wachtte. Alleen de nieuwste bewaren, zodat een nieuwere een oudere vervangt die nooit gebruikt is, heet coalescing (samenvoegen). Vergelijk een queue, een waar offer iets aan toevoegt en waar take vooraan uit leest. Een queue zou elke versie om de beurt afgeven, en zolang zijn kijker traag is, zou hij per score met één snapshot groeien. Een plek bevat één snapshot, hoe ver zijn kijker ook achterraakt, dus een trage kijker kost de server evenveel geheugen als een snelle.

Versies 4, 5 en 6 worden aangeboden voordat de thread van de kijker iets ophaalt. Wat geeft take terug?

Een event wekt de verzender

De thread die een kijker zijn snapshots zal sturen, moet slapen zolang de plek leeg is en wakker worden zodra er iets aankomt. threading.Event doet dat. Een event is een vlag die gewist begint: set() zet hem, clear() wist hem, is_set() meldt hem, en wait(timeout=...) blokkeert totdat de vlag gezet is of de time-out verloopt. wait geeft True terug als het event gezet werd en False als de tijd eerst op was. In CPython keert een wait met een time-out terug zodra een andere thread set() aanroept; hij slaapt de time-out niet helemaal uit. De outbox rekent precies daarop.

In Networks with Python gebruikte rendezvous in fakes.py een event, en die les zei dat je er zelf geen nodig zou hebben. Nu wel. Hier wacht de ene thread en zet een andere het 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 drukt meteen woken by set() af. Haal de regel ready.set() weg, en hij drukt na twee seconden de andere regel af.

Niets zet een event gedurende een halve seconde. Wat geeft event.wait(timeout=0.5) terug, en wanneer?

wait_and_take(outbox, timeout) is de helft van de verzender: wacht op het event van de outbox met de time-out die hij kreeg, en geef daarna take(outbox) terug. Als er niets kwam, is de plek leeg en geeft take None terug, zodat de aanroeper hoort dat de tijd op is. De time-out doet ertoe, ook al wekt het event de thread vroeg: les 3 gebruikt de stille momenten om naar de verbinding van de kijker te kijken.

Opslaan en zetten samen

Het event moet gezet zijn precies wanneer er een snapshot wacht. Als het gewist was terwijl er een snapshot wachtte, zou de verzender door een score heen kunnen slapen die hij moet sturen. Daarom slaat offer de snapshot op en zet hij het event binnen één with outbox["lock"]:-blok, en leest take de plek, maakt hem leeg en wist hij het event ook binnen één blok. Dan kan geen andere thread zien dat de plek en het event het oneens zijn.

Neem een take die het event wist nadat zijn with-blok is geëindigd. Tussen het einde van het blok en de clear() kan een andere thread een offer uitvoeren: die slaat een nieuwe snapshot op en zet het event. Daarna wist de late clear() het. Er wacht nu een snapshot onder een gewist event, en de thread van de kijker kan gaan slapen in plaats van hem te sturen.

take wist het event nadat zijn with-blok is geëindigd. Wat kan er misgaan?

wait_and_take wacht buiten de lock. Terwijl hij wacht, moet een offer de lock kunnen nemen en de plek kunnen vullen.

Twee locks, één volgorde

Het bord heeft zijn lock, en elke outbox heeft nu zijn eigen. Twee threads die twee locks in omgekeerde volgorde nemen, kunnen eeuwig op elkaar wachten. Dat kan hier niet gebeuren, omdat de volgorde altijd dezelfde is. offer draait terwijl apply_score de lock van het bord vasthoudt: eerst de lock van het bord, dan een outbox-lock. De thread van een kijker neemt de lock van het bord alleen om binnen te komen en te vertrekken, nooit terwijl hij de lock van zijn outbox vasthoudt. Geen thread houdt ooit een outbox-lock vast en vraagt dan om de lock van het bord.

Je opdracht

Schrijf in scoreboard_server.py de vier functies van les 2:

  1. make_outbox(): een nieuwe dict met "snapshot": None, een lock van threading.Lock() en een event van threading.Event();

  2. offer(outbox, snapshot): sla binnen with outbox["lock"]: de snapshot op en zet het event;

  3. take(outbox): lees binnen with outbox["lock"]: de plek, zet hem op None, wis het event en geef terug wat je las;

  4. wait_and_take(outbox, timeout): wacht op het event met timeout=timeout, en geef daarna take(outbox) terug.

Roep threading.Lock() en threading.Event() aan via de , binnen make_outbox. De controle vervangt de module threading door een stand-in die elke lock en elk event vastlegt, en die kan geen lock zien die wordt gemaakt als het bestand wordt geïmporteerd, of een die met from threading import is geïmporteerd.

Uitvoeren

Klik op Run. De twee secties van les 2 in scoreboard_demo.py gebruiken nu je functies. De eerste biedt drie snapshots aan één outbox aan en haalt twee keer op; de tweede start een thread in wait_and_take(outbox, 2.0) en biedt daarna versie 9 aan. Een afgemaakte versie, op de site uitgevoerd, drukte af:

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

De wachtende thread haalde versie 9 op zonder zijn twee seconden uit te slapen: zodra het event gezet was, keerde wait terug. Klik daarna op Submit.

Les 3 geeft elke kijker een eigen outbox en verplaatst elke send naar de eigen thread van die kijker.

Opdracht

Schrijf in scoreboard_server.py de vier van les 2:

  1. make_outbox(): geef {"snapshot": None, "lock": threading.Lock(), "event": threading.Event()} terug, bij elke aanroep een nieuwe lock en een nieuw event.

  2. offer(outbox, snapshot): zet binnen with outbox["lock"]: snapshot in outbox["snapshot"], ter vervanging van wat er wacht, en zet outbox["event"].

  3. take(outbox): lees binnen with outbox["lock"]: outbox["snapshot"], zet het op None, wis het event, en geef terug wat je las (None als er niets wachtte).

  4. wait_and_take(outbox, timeout): roep outbox["event"].wait(timeout=timeout) aan buiten de lock, en geef daarna take(outbox) terug.

Schrijf threading.Lock() en threading.Event() via de , binnen make_outbox: de controle vervangt de module threading van scoreboard_server door een stand-in die elke lock en elk event vastlegt, en weigert een lock of event die wordt gemaakt als het bestand wordt geïmporteerd, of een die met from threading import is binnengehaald.

Klik op Run: de twee secties van les 2 drukken take: {'type': 'snapshot', 'version': 3, 'scores': {}}, take again: None and the event is set: False en the waiting thread took version 9 af.

Klik daarna op Submit. De controle biedt snapshots aan die je nog niet hebt gezien (versie 17, 18 en 19) aan een nieuwe outbox en verwacht dat take versie 19 teruggeeft, dat een tweede take None teruggeeft, dat het event na elke offer gezet is en na elke take gewist, en dat elk zetten en wissen gebeurt terwijl de lock van die outbox wordt vastgehouden. Ze biedt duizend snapshots aan en verwacht er één op de plek, nooit een . Ze roept wait_and_take(outbox, 0.75) aan en verwacht dat 0.75 bij wait aankomt, dat de wachtende snapshot terugkomt, en None als er niets wacht. acquire() en release() in plaats van with zijn prima, en een wait_and_take die None teruggeeft zonder op te halen als wait False teruggeeft ook.