← Python/Concurrency
Les 5 van 8
MODULE 12 · 05◷ 30 min

Stoppen afwachten en eigen middelen opruimen

Na deze les kun je
  • Annulering als verzoek onderscheiden van een direct gestopte taak.
  • Eigen opruiming met finally uitvoeren bij succes, gewone fouten en annulering.
  • CancelledError na opruiming laten doorgaan en de gestopte taak afwachten.

Een gebruiker sluit een rapport terwijl een bron nog wordt gelezen. Het werk heeft dan geen nuttig doel meer, maar er kan nog wel iets openstaan dat de coroutine zelf moet afsluiten. Stoppen en opruimen horen daarom bij dezelfde levenscyclus.

Stoppen is een verzoek

Met task.cancel() vraag je om annulering. Dat bewijst nog niet dat de taak al gestopt is. Python laat bij een volgende gelegenheid een CancelledError in de taak optreden, vaak rond een await die controle aan de eventloop teruggeeft.

Async code werkt hierbij coöperatief: taken geven op zulke punten ruimte aan ander werk. Een lange gewone rekenlus zonder onderbreking wordt niet plots door een andere coroutine overgenomen. Het woord async maakt die lus dus niet vanzelf responsief.

Bewaar de taakreferentie, vraag om stoppen en wacht vervolgens op diezelfde taak. Pas daarna weet de aanroeper hoe het werk is geëindigd. Eventuele eigen opruimcode kan daarbij eerst nog moeten lopen.

Opruiming vóór de afloopmelding
import asyncio

async def work(entered):
    try:
        entered.set()
        await asyncio.Event().wait()
    finally:
        print("opgeruimd")

async def main():
    entered = asyncio.Event()
    task = asyncio.create_task(work(entered))
    await entered.wait()
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("gestopt")

asyncio.run(main())
Verwachte uitvoer
opgeruimd
gestopt

De oefentaak meldt met een Event dat zij werkelijk begonnen is. Daarna vraagt de aanroeper om annulering en wacht erop. Daardoor verschijnt opgeruimd vóór de melding van de aanroeper dat het werk gestopt is.

Opruimen hoort in finally

Je kent try/finally uit foutafhandeling en bestanden. Hetzelfde patroon geldt in async code. Een finally-blok wordt ook uitgevoerd wanneer een afgewachte bewerking een gewone uitzondering of CancelledError veroorzaakt.

Plaats het werk waarvoor je verantwoordelijkheid draagt binnen de try. Het afsluiten hoort in finally. Alleen onderaan het normale pad afsluiten werkt niet wanneer een fout of annulering de tussenliggende regels overslaat.

Wanneer afsluiten zelf async is, kan daar een await voor nodig zijn. De huidige oefening gebruikt bewust een gewone afsluitfunctie die niet faalt. Zo richt je je op één verantwoordelijkheid: haar precies eenmaal uitvoeren ongeacht de manier waarop het overzetten eindigt.

Een annulering is geen gewone invoerfout

CancelledError erft rechtstreeks van BaseException. Een except Exception vangt haar daarom niet. Dat helpt voorkomen dat een algemene handler voor leesfouten een stopverzoek als gewoon herstelbaar probleem behandelt.

Finally loopt terwijl annulering doorgaat
import asyncio

async def leave():
    try:
        raise asyncio.CancelledError("stop")
    except Exception:
        print("gewone fout")
    finally:
        print("finally")

async def main():
    try:
        await leave()
    except asyncio.CancelledError as error:
        print("annulering:", str(error))

asyncio.run(main())
Verwachte uitvoer
finally
annulering: stop

In het voorbeeld komt de gewone except-handler niet aan bod. Het finally-blok loopt wel en de aanroeper ontvangt de annulering. Daarmee blijft zichtbaar dat stoppen een andere uitkomst is dan succesvol lege gegevens teruggeven.

Soms wil je een annulering expliciet opmerken, bijvoorbeeld om iets te registreren. Gebruik dan een gerichte handler en laat haar na het noodzakelijke opruimen met raise doorgaan. Zonder zo'n beslissing kan inslikken ook de afspraken van TaskGroup of een timeout verstoren.

Een klein overzetproces

De oefenfunctie krijgt drie bewerkingen mee. read_piece() is async en levert een tekstdeel of None om het einde aan te geven. write_piece(piece) is async en schrijft dat ene deel; close() sluit de eigen oefenbron af.

Een lege tekst is een geldig deel. Test dus op is None om het einde te herkennen, niet op de waarheid van de tekst. Wacht ieder lezen en schrijven af en tel een deel pas mee nadat zijn schrijfaanroep succesvol is afgerond.

Veelgemaakte fout: stoppen als succes teruggeven

Een brede handler die bij iedere fout nul teruggeeft, laat een afgebroken overdracht op een geslaagde lege overdracht lijken. De aanroeper verliest dan zowel het stopverzoek als de oorzaak. Laat gewone lees- en schrijffouten daarom ook doorgaan als hetzelfde exceptionobject.

Alle checkpoints bekijken deze ene overdrachtsfunctie. Alleen een normaal bereikte None levert een telling op. In alle paden sluit je de eigen bron af; de afsluitfunctie van deze oefening kan zelf geen nieuwe fout veroorzaken.

AAN DE SLAG 0/4
1

Normaal overzetten en afsluiten

Bouw async transfer(read_piece, write_piece, close). Lees telkens één deel en schrijf het afgewacht weg tot read_piece None geeft. Geef het aantal succesvol geschreven delen terug en roep close precies eenmaal aan. Een lege tekst is een geldig deel. Roep close pas aan nadat het laatste gestarte lees- of schrijfwerk is beëindigd, precies eenmaal vóór terugkeer.

2

Een leesfout behouden

Laat een gewone fout uit read_piece als hetzelfde exceptionobject doorgaan. Ook wanneer het eerste lezen faalt of al een deel geschreven is, moet close precies eenmaal lopen. Start na de fout of annulering geen nieuw lees- of schrijfwerk en sluit de bron pas na de afgebroken I/O.

3

Een schrijffout behouden

Laat een gewone fout uit write_piece als hetzelfde exceptionobject doorgaan. Reeds succesvol geschreven delen blijven behouden; na de fout sluit je precies eenmaal af zonder een succesgetal terug te geven. Start na de fout of annulering geen nieuw lees- of schrijfwerk en sluit de bron pas na de afgebroken I/O.

4

Een stopverzoek doorgeven

Laat annulering tijdens lezen of schrijven als CancelledError doorgaan. De eigen close-aanroep is afgerond voordat de gestopte overdrachtstaak naar haar aanroeper terugkeert. Sluit precies eenmaal af. Start na de fout of annulering geen nieuw lees- of schrijfwerk en sluit de bron pas na de afgebroken I/O.

Samenvatting

  • Cancel vraagt om stoppen; await op de taak maakt de afloop zichtbaar.
  • Finally geeft eigen middelen vrij bij succes, gewone fouten en annulering.
  • Laat CancelledError en onopgeloste fouten doorgaan in plaats van succes te verzinnen.

Hierna begrens je wachten met een timeout en bekijk je waar de TimeoutError verschijnt.

transfer.py
UitvoerPython
›
›_

Hier komt je resultaat.

Voer je code uit en ontdek
wat je hebt gemaakt.

0/4 stappen
EVEN MEEKIJKEN

Bekijk hoe het kan.

Je mag de oplossing bekijken. Deze poging telt dan niet mee als afgeronde les. Met Reset kun je daarna opnieuw zelf oefenen.