0%

Een voorproefje van async · oefening

De kijkershandler als coroutine

Je kunt nu een schrijven, een andere awaiten en de buitenste draaien. Deze les herschrijft de kijkershandler van het eindproject in die stijl, en alleen de kijkershandler: het bord, de broadcast en de scorebijhouder blijven waar hoofdstuk 10 ze achterliet. Hier is de uit hoofdstuk 10, en daarna de coroutine die je gaat schrijven:

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

Veel blijft hetzelfde. De snapshot bij binnenkomst komt eerst. Eén verzender per kijker, de handler zelf, stuurt alleen de nieuwste snapshot. Een verbinding die eindigt, werpt ConnectionClosed op, wat de handler stil beëindigt. En de quiz eindigt op dezelfde manier: de laatste snapshot, daarna een close met 1000. Wat verandert, is elke plek waar de handler wacht.

Waarom niet Event.wait()

In hoofdstuk 8 wachtte wait_and_take met de Event.wait(timeout=...) van de outbox. Die aanroep blokkeert: de thread die hem doet, doet niets anders tot het event gezet is of de time-out verstrijkt. Met een thread per kijker was dat de bedoeling. Op een is het een ramp, want de thread die hij blokkeert, is de enige thread van de lus, en elke andere kijker wachtte erop dat die thread hem zou draaien.

pl_ws.py, een programma dat voor deze cursus is geschreven en elke client speelt, laat het verschil zien met twee eigen handlers. Naast elke handler telt een ticker-coroutine elke 0,05 s. In een run draaide de ticker 9 keer terwijl de ene handler wachtte met await asyncio.sleep(0.5), en 0 keer terwijl de andere wachtte met time.sleep(0.5). Beide wachtten een halve seconde. Alleen de eerste gaf de controle terug terwijl hij wachtte.

Waarom kan de coroutine niet op de volgende snapshot wachten met de Event.wait(timeout=...) van de outbox?

next_snapshot()

Daarom krijgt de handler in plaats daarvan een coroutinefunctie om te awaiten, next_snapshot, en dit hoofdstuk levert die mee. next_snapshot() awaiten geeft eerst de snapshot bij binnenkomst, daarna de nieuwste snapshot elke keer dat er een nieuwere is, en None zodra de quiz voorbij is. Zolang er niets nieuws is aangekomen, wacht het zonder te blokkeren: de handler pauzeert bij die await, en de lus bedient andere kijkers. Het is gebouwd op hulpmiddelen van asyncio die dit voorproefje niet behandelt, waaronder een asyncio.Event, waarop je kunt awaiten in plaats van wachten.

Lees het als de outbox uit hoofdstuk 8 en het einde uit hoofdstuk 10, gezien van binnen de handler. Alleen de nieuwste is de regel van de outbox. None is de regel van de laatste versie: de kijker heeft de laatste snapshot gekregen, dus sluit met 1000.

Eén ding is weggelaten. De handler uit hoofdstuk 10 keek naar de verbinding als hij stil was, om een kijker op te merken die weg was. Hier wordt een kijker die vertrekt opgemerkt bij de volgende send, die ConnectionClosed opwerpt. De verbinding in de gaten houden en tegelijk op een snapshot wachten, vraagt dat er binnen één handler twee dingen tegelijk gebeuren, en daar zijn voor nodig, en tasks staan op de van les 4.

Drie awaits

In websockets.asyncio zijn send, recv en close op een verbinding coroutinefuncties, dus elk ervan wordt ge-: await connection.send(text), await connection.close(1000, "the quiz is over"). Vergeet je de await bij een send, dan komt de waarschuwing uit les 2 terug: de aanroep maakte alleen een coroutine, niets draaide die, en de kijker ontving niets.

Je handler awaitet precies drie dingen: next_snapshot(), connection.send(...) en connection.close(...). Als je op Submit klikt, draait de controle je coroutine stap voor stap met stand-ins, zodat ze elke plek kan zien waar je handler de controle teruggeeft. Omdat ze je coroutine zonder event loop draait, mag je handler alleen die drie awaiten; iets awaiten wat een lus nodig heeft, zoals asyncio.sleep(0.1), wordt gemeld als fout.

Je handler schrijft connection.send(text) zonder await. Wat ontvangt de kijker?

De asyncio-server, alleen om te lezen

Dit is de vorm van een server die op websockets.asyncio is gebouwd, om te herkennen. Je schrijft hem in deze cursus niet:

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())

Vergelijk hem met hoofdstuk 6. serve neemt een handler met één , de verbinding, zoals de synchrone server deed, maar de handler is een coroutinefunctie. Het with-blok wordt async with, een with waarvan het opzetten en opruimen wordt ge-await, en serve_forever wordt ge-await in plaats van aangeroepen. Eén asyncio.run onderaan start het allemaal. Om elk bericht van een client te lezen, schrijft asyncio-code async for message in connection:, een for-lus waarvan elke stap wordt ge-await. Beide vormen komen in deze cursus alleen voor in fragmenten zoals dit.

Je opdracht

Schrijf in async_scoreboard.py viewer_handler(connection, next_snapshot). Lus: await next_snapshot(); als dat None gaf, await je connection.close(1000, "the quiz is over") en keer je terug; anders await je connection.send(...) met scoreboard_messages.encode_message(snapshot). Zet de lus in try en except ConnectionClosed, en keer vanuit de except stil terug. Dat zijn ongeveer negen regels.

Klik op Run. De eerste sectie van les 3 draait je handler onder asyncio.run met een stand-in-verbinding waarvan send en close coroutinefuncties zijn die vastleggen wat ze krijgen, en een next_snapshot die drie snapshots geeft en daarna None. In de tweede laat pl_ws.py je handler met websockets.asyncio drie echte kijkers op bedienen, en draait het daarna de tickervergelijking. Een afgemaakte versie, op de site uitgevoerd, drukte af:

--- 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

Drie kijkers werden tegelijk bediend, op één thread, door jouw coroutine.

De Run zegt dat elke handler op de hoofdthread draaide. Hoe werden drie kijkers tegelijk bediend?

Klik daarna op Submit. De volgende les trekt de grens van dit voorproefje: wat een echte cursus in asyncio zou toevoegen.

Opdracht

Schrijf in async_scoreboard.py viewer_handler(connection, next_snapshot) als coroutinefunctie:

  1. in een next_snapshot(). Dat geeft eerst de snapshot bij binnenkomst, daarna elke nieuwere snapshot, en None als de quiz voorbij is.

  2. Als het None geeft, await je connection.close(1000, "the quiz is over") (de redentekst kies je zelf) en keer je terug.

  3. Anders await je connection.send(scoreboard_messages.encode_message(snapshot)), en vraag je daarna opnieuw.

  4. Zet de lus in een try waarvan de except ConnectionClosed stil terugkeert. Roep close niet aan na ConnectionClosed: die verbinding is al geëindigd.

Je handler awaitet alleen next_snapshot(), connection.send(...) en connection.close(...). Submit draait hem stap voor stap zonder , dus iets anders awaiten wat er een nodig heeft (bijvoorbeeld asyncio.sleep(0.1)) wordt gemeld als fout. Wacht niet met time.sleep, threading of een Event.wait(...): binnen een blokkeren die de enige thread van de lus. label en two_labels uit les 2 worden niet gecontroleerd.

Klik op Run: de sectie met de stand-in drukt sent version 0 tot en met sent version 2 en closed 1000 the quiz is over af, en pl_ws.py laat je handler drie kijkers bedienen.

Klik daarna op Submit. De controle geeft je handler stand-ins en snapshots die je nog niet hebt gezien, met versies die getallen overslaan en een teamnaam met een accent. Elke snapshot moet worden verstuurd, op volgorde, als de -tekst van die snapshot, elk na de next_snapshot() die hem gaf; next_snapshot() moet na de laatste nog één keer worden ge-await, en daarna moet de verbinding één keer worden gesloten, met 1000, zonder dat er na de close iets wordt verstuurd. Als een send ConnectionClosedError opwerpt, moet de handler stil eindigen zonder om nog een snapshot te vragen, en als de eerste next_snapshot() None geeft, moet hij sluiten met 1000 en niets sturen. De waarschuwing “was never awaited” van Python telt als fout. Een while True-lus of een lus op een vlag, of break na de close, een ge-awaite asyncio.sleep(0), en een send die via een naam wordt ge-await, zijn allemaal prima.