0%

Slow Clients and Lost Connections · practice

Keep Only the Latest Snapshot

Lesson 1 settled on a policy: for each viewer, keep only the newest snapshot. A snapshot holds the whole board, so when a newer one arrives before the older one was sent, the older one can go. This lesson builds the place where that snapshot waits, an outbox, with four small . No viewer uses an outbox yet; Lesson 3 gives one to each.

One slot

An outbox is a with three keys:

  • "snapshot": the snapshot waiting to be sent, or None when nothing waits;

  • "lock": a threading.Lock of its own, for the moments the slot changes;

  • "event": a threading.Event, which the next section introduces, set exactly while a snapshot waits.

offer(outbox, snapshot) puts a snapshot in the slot, replacing whatever was there, and take(outbox) returns what waits and leaves the slot empty. Here is one outbox through three offers and two takes:

StepSlot afterwardsEvent afterwardstake returns
offer version 1version 1set
offer version 2version 2set
takeemptyclearversion 2
offer version 3version 3set
takeemptyclearversion 3

Version 1 was never taken: version 2 replaced it while it waited. Keeping only the newest , so that a newer one replaces an older one that was never used, is called coalescing. Compare a queue, a that offer appends to and take reads from the front. A queue would hand over every version in turn, and while its viewer is slow it would grow by one snapshot per score. A slot holds one snapshot however far behind its viewer falls, so a slow viewer costs the server the same memory as a fast one.

Versions 4, 5 and 6 are offered before the viewer's thread takes. What does take return?

An event wakes the sender

The thread that will send a viewer its snapshots needs to sleep while the slot is empty and wake as soon as something arrives. threading.Event does that. An event is a flag that starts clear: set() raises it, clear() lowers it, is_set() reports it, and wait(timeout=...) blocks until the flag is set or the timeout runs out. wait returns True if the event was set and False if the time ran out first. In CPython a wait with a timeout returns as soon as another thread calls set(); it does not sleep out the whole timeout. The outbox relies on exactly that.

In Networks with Python, rendezvous in fakes.py used an event, and that lesson said you would not need one yourself. Now you do. Here is one thread waiting and another setting:

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 prints woken by set() at once. Remove the ready.set() line and it prints the other line after two seconds.

Nothing sets an event for half a second. What does event.wait(timeout=0.5) return, and when?

wait_and_take(outbox, timeout) is the sender’s half: wait on the outbox’s event with the timeout it was given, then return take(outbox). If nothing came, the slot is empty and take returns None, so the caller learns that the time ran out. The timeout matters even though the event wakes the thread early: Lesson 3 uses the quiet moments to look at the viewer’s connection.

Store and set together

The event must be set exactly when a snapshot waits. If it were clear while a snapshot waited, the sender could sleep through a score it should send. So offer stores the snapshot and sets the event inside one with outbox["lock"]: block, and take reads the slot, empties it and clears the event inside one block too. Then no other thread can see the slot and the event disagree.

Consider a take that clears the event after its with block ends. Between the end of the block and the clear(), another thread can run an offer: it stores a new snapshot and sets the event. Then the late clear() lowers it. A snapshot now waits under a clear event, and the viewer’s thread may sleep instead of sending it.

take clears the event after its with block ends. What can go wrong?

wait_and_take waits outside the lock. While it waits, an offer must be able to take the lock and fill the slot.

Two locks, one order

The board has its lock, and every outbox now has its own. Two threads that take two locks in opposite orders can wait for each other forever. That cannot happen here, because the order is always the same. offer will run while apply_score holds the board lock: board lock first, then an outbox lock. A viewer’s thread takes the board lock only to join and to leave, never while it holds its outbox’s lock. No thread ever holds an outbox lock and then asks for the board lock.

Your task

In scoreboard_server.py, write the four Lesson 2 functions:

  1. make_outbox(): a new dict with "snapshot": None, a lock from threading.Lock() and an event from threading.Event();

  2. offer(outbox, snapshot): inside with outbox["lock"]:, store the snapshot and set the event;

  3. take(outbox): inside with outbox["lock"]:, read the slot, set it to None, clear the event and return what you read;

  4. wait_and_take(outbox, timeout): wait on the event with timeout=timeout, then return take(outbox).

Call threading.Lock() and threading.Event() through the , inside make_outbox. The check replaces the threading module with a stand-in that records every lock and event, and it cannot see one made when the file is imported or one imported with from threading import.

Run

Press Run. The two Lesson 2 sections of scoreboard_demo.py now use your functions. The first offers three snapshots to one outbox and takes twice; the second starts a thread in wait_and_take(outbox, 2.0) and then offers version 9. A finished version, run on the site, printed:

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

The waiting thread took version 9 without sleeping out its two seconds: once the event was set, wait returned. Then press Submit.

Lesson 3 gives every viewer its own outbox and moves each send onto that viewer’s own thread.

Task

In scoreboard_server.py, write the four Lesson 2 :

  1. make_outbox(): return {"snapshot": None, "lock": threading.Lock(), "event": threading.Event()}, a new lock and a new event on every call.

  2. offer(outbox, snapshot): inside with outbox["lock"]:, put snapshot in outbox["snapshot"], replacing anything waiting, and set outbox["event"].

  3. take(outbox): inside with outbox["lock"]:, read outbox["snapshot"], set it to None, clear the event, and return what you read (None when nothing waited).

  4. wait_and_take(outbox, timeout): call outbox["event"].wait(timeout=timeout) outside the lock, then return take(outbox).

Write threading.Lock() and threading.Event() through the , inside make_outbox: the check replaces scoreboard_server’s threading module with a stand-in that records every lock and event, and it refuses a lock or event made when the file is imported, or one brought in with from threading import.

Press Run: the two Lesson 2 sections print take: {'type': 'snapshot', 'version': 3, 'scores': {}}, take again: None and the event is set: False and the waiting thread took version 9.

Then press Submit. The check offers snapshots you have not seen (versions 17, 18 and 19) to a new outbox and expects take to return version 19, a second take to return None, the event to be set after each offer and clear after each take, and every set and clear to happen while that outbox’s lock is held. It offers a thousand snapshots and expects one in the slot, never a . It calls wait_and_take(outbox, 0.75) and expects 0.75 to reach wait, the waiting snapshot to come back, and None when nothing waits. acquire() and release() in place of with are fine, and so is a wait_and_take that returns None without taking when wait returns False.