A Taste of Async · practice
The Viewer Handler as a Coroutine
You can now write a
Chapter 10, a thread per viewer:
snapshot = wait_and_take(outbox, CHECK_EVERY)
nothing yet: notice_close(connection)
connection.send(text)
last version sent: connection.close(1000, ...)
except ConnectionClosed: leave quietly
Chapter 11, a coroutine:
snapshot = await next_snapshot()
(next_snapshot waits until there is one)
await connection.send(text)
None: await connection.close(1000, ...)
except ConnectionClosed: leave quietly
Much stays the same. The join snapshot comes first. One sender per viewer, the handler itself, sends only the newest snapshot. A connection that ends raises ConnectionClosed, which ends the handler quietly. And the quiz ends the same way: the last snapshot, then a close with 1000. What changes is every place the handler waits.
Why not Event.wait()
In Chapter 8, wait_and_take waited with the outbox’s Event.wait(timeout=...). That call blocks: the thread that makes it does nothing else until the event is set or the timeout passes. With a thread per viewer, that was the point. On an
pl_ws.py, a program written for this course that plays every client, shows the difference with two handlers of its own. Beside each, a ticker coroutine counts every 0.05 s. In a run, the ticker ran 9 times while one handler waited with await asyncio.sleep(0.5), and 0 times while the other waited with time.sleep(0.5). Both waited half a second. Only the first one handed control back while it waited.
Why can the coroutine not wait for the next snapshot with the outbox's Event.wait(timeout=...)?
next_snapshot()
So the handler is given a coroutine next_snapshot, and this chapter supplies it. Awaiting next_snapshot() gives the join snapshot first, then the newest snapshot each time there is a newer one, and None once the quiz is over. While nothing new has arrived, it waits without blocking: the handler pauses at that await, and the loop serves other viewers. It is built on asyncio tools this taste does not teach, an asyncio.Event among them, which can be awaited instead of waited on.
Read it as Chapter 8’s outbox and Chapter 10’s ending, seen from inside the handler. Newest only is the outbox’s rule. None is the last-version rule: the viewer has been given the last snapshot, so close with 1000.
One thing is left out. The Chapter 10 handler looked at the connection while idle, to notice a viewer that had gone. Here a viewer who leaves is noticed at the next send, which raises ConnectionClosed. Watching the connection while also waiting for a snapshot takes two things happening at once inside one handler, which needs tasks, and tasks belong to Lesson 4’s
Three awaits
In websockets.asyncio, send, recv and close on a connection are coroutine functions, so each is awaited: await connection.send(text), await connection.close(1000, "the quiz is over"). Forget the await on a send and Lesson 2’s warning comes back: the call only made a coroutine, nothing ran it, and the viewer received nothing.
Your handler awaits exactly three things: next_snapshot(), connection.send(...) and connection.close(...). When you press Submit, the check runs your coroutine one step at a time with stand-ins, so it can see every place your handler hands control back. Because it runs your coroutine with no event loop, your handler may await only those three; awaiting anything that needs a loop, such as asyncio.sleep(0.1), is reported as a mistake.
Your handler writes connection.send(text) without await. What does the viewer receive?
The asyncio server, read only
This is the shape of a server built on websockets.asyncio, for recognition. You will not write it in this course:
import asyncio
from websockets.asyncio.server import serve
async def handler(connection):
await connection.send("hello")
async def main():
async with serve(handler, "127.0.0.1", 0) as server:
await server.serve_forever()
asyncio.run(main())
Compare it with Chapter 6. serve takes a handler with one with block becomes async with, a with whose setup and cleanup are awaited, and serve_forever is awaited instead of called. One asyncio.run at the bottom starts it all. To read every message from a client, asyncio code writes async for message in connection:, a for loop whose every step is awaited. Both forms appear only in fragments like this one in this course.
Your task
In async_scoreboard.py, write viewer_handler(connection, next_snapshot). Loop: await next_snapshot(); if it gave None, await connection.close(1000, "the quiz is over") and return; otherwise await connection.send(...) with scoreboard_messages.encode_message(snapshot). Wrap the loop in try and except ConnectionClosed, and return quietly from the except. That is about nine lines.
Press Run. The first Lesson 3 section runs your handler under asyncio.run with a stand-in connection whose send and close are coroutine functions that record what they are given, and a next_snapshot that gives three snapshots and then None. The second has pl_ws.py serve your handler with websockets.asyncio to three real viewers on
--- Lesson 3: your handler with a stand-in connection
sent version 0
sent version 1
sent version 2
closed 1000 the quiz is over
--- Lesson 3: your handler served by websockets.asyncio
pl_ws.py, a program written for this course, serves your handler with websockets.asyncio and plays three viewers:
scenario async-viewers: your async viewer_handler served by websockets.asyncio; three viewers; versions 1 to 3, then the end of the quiz
viewer A received versions [0, 1, 2, 3], then the server closed it with 1000 'the quiz is over'
viewer B received versions [0, 1, 2, 3], then the server closed it with 1000 'the quiz is over'
viewer C received versions [0, 1, 2, 3], then the server closed it with 1000 'the quiz is over'
every handler ran on the main thread: True
threads in this process while serving: 1
now this program's own code: a ticker that counts every 0.05 s, beside a handler that waits 0.5 s
while the handler waited with await asyncio.sleep(0.5), the ticker ran 9 times
while the handler waited with time.sleep(0.5), the ticker ran 0 times
Three viewers were served at once, on one thread, by your coroutine.
A websockets.asyncio server runs your coroutine viewer_handler for three viewers. Every handler runs on the main thread, and the process has one thread while serving. How were three viewers served at once?
Then press Submit. The next lesson draws the edge of this taste: what a real course in asyncio would add.
Task
In async_scoreboard.py, write viewer_handler(connection, next_snapshot) as a
In a
, next_snapshot(). It gives the join snapshot first, then each newer snapshot, andNonewhen the quiz is over.When it gives
None, awaitconnection.close(1000, "the quiz is over")(the reason text is yours) and return.Otherwise await
connection.send(scoreboard_messages.encode_message(snapshot)), then ask again.Put the loop in a
trywhoseexcept ConnectionClosedreturns quietly. Do not callcloseafterConnectionClosed: that connection has already ended.
Your handler awaits only next_snapshot(), connection.send(...) and connection.close(...). Submit runs it one step at a time without an asyncio.sleep(0.1)) is reported as a mistake. Do not wait with time.sleep, threading or an Event.wait(...): inside a coroutine those block the loop’s only thread. label and two_labels from Lesson 2 are not checked.
Press Run: the stand-in section prints sent version 0 to sent version 2 and closed 1000 the quiz is over, and pl_ws.py serves your handler to three viewers.
Then press Submit. The check gives your handler stand-ins and snapshots you have not seen, with versions that skip numbers and a team name with an accent. Every snapshot must be sent, in order, as the next_snapshot() that gave it; next_snapshot() must be awaited once more after the last one, and then the connection closed once, with 1000, with nothing sent after the close. When a send raises ConnectionClosedError the handler must end quietly without asking for another snapshot, and when the first next_snapshot() gives None it must close with 1000 and send nothing. Python’s “was never awaited” warning counts as a mistake. A while True loop or a loop on a flag, break after the close, an awaited asyncio.sleep(0), and a send awaited through a name are all fine.