8.5. Expirări și anulare

Anularea este numele dat de asyncio pentru „oprește execuția acestei corutine, ridică o excepție în interiorul ei astfel încât să aibă șansa de a se curăța și elimin-o din planificator”. Expirările sunt cel mai frecvent motiv de a anula ceva; task.cancel() manual este celălalt.

8.5.1. Anularea unei sarcini

Apelarea Task.cancel programează ridicarea asyncio.CancelledError în interiorul corutinei care rulează, la următorul ei await. Corutina este liberă să ignore anularea (să captureze excepția și să continue) sau să o onoreze – alegerea uzuală este de a o onora după rularea codului de curățare:

async def network_worker(stream):
    try:
        while True:
            data = await stream.read(64)
            # ...
    finally:
        stream.close()

O clauză finally simplă este tiparul cel mai curat: curățarea se execută indiferent dacă corutina a ieșit normal, a ridicat o excepție fără legătură sau a fost anulată. CancelledError se propagă înapoi prin finally, bucla vede că sarcina este încheiată, iar apelantul lui await task vede anularea.

Capturarea explicită a CancelledError este de asemenea în regulă atunci când aplicația dorește să facă ceva specific cu ea – să o înregistreze, să cedeze resursa curat etc. Regula este: reridic-o după ce rulează curățarea. Înghițirea CancelledError menține corutina în viață când apelantul ei i-a cerut să se oprească, ceea ce este aproape întotdeauna o eroare:

async def worker():
    try:
        await long_running_thing()
    except asyncio.CancelledError:
        log("worker cancelled, cleaning up")
        close_resources()
        raise          # << this line is the important one

8.5.2. Expirări cu wait_for

asyncio.wait_for() încapsulează un obiect awaitable cu un termen limită. Dacă obiectul awaitable se încheie în limita timpului, rezultatul lui este returnat. Dacă nu, obiectul awaitable este anulat, iar apelantul primește asyncio.TimeoutError

try:
    frame = await asyncio.wait_for(grab_frame(), timeout=2)
except asyncio.TimeoutError:
    print("camera took too long")

Argumentul în secunde acceptă un număr cu virgulă. Pentru termene limită exprimate în milisecunde, asyncio.wait_for_ms() acceptă un număr întreg de milisecunde – o extensie MicroPython care se aliniază cu comenzile de temporizare cu granularitate de milisecundă ale firmware-ului.

Intern, wait_for face exact ceea ce ar face anularea manuală: când termenul limită expiră, apelează cancel() pe sarcina încapsulată, CancelledError este ridicată în interiorul corutinei, clauzele finally se execută, iar odată ce curățarea este gata, excepția este tradusă într-o TimeoutError pentru apelant.

Asta înseamnă că o corutină care captează și ignoră CancelledError va anula expirarea – termenul limită a expirat, dar corutina a refuzat să se oprească, iar wait_for nu o poate forța. Regula anterioară se aplică și aici: captează CancelledError doar pentru a rula curățarea, apoi reridic-o.

8.5.3. Anulare prin gather

Anularea se propagă în jos prin gather(). Dacă sarcina care așteaptă un apel gather este anulată, fiecare obiect awaitable care încă rulează în interiorul gather-ului este de asemenea anulat – fiecare primește șansa de a se curăța prin propriile clauze finally înainte ca anularea să urce înapoi la apelant.

Combinat cu expirările, acesta este modul standard de a pune un termen limită pe un grup de operațiuni:

await asyncio.wait_for(
    asyncio.gather(a(), b(), c()),
    timeout=5,
)

Fie fiecare suboperațiune se încheie în cinci secunde, fie toate sunt anulate împreună.

8.5.4. Oprirea unei aplicații

Anularea este de asemenea modul în care o aplicație reală se oprește curat. Tiparul este consecvent în toate scripturile: main captează referințele sarcinilor de fundal de lungă durată pe care le-a pornit, rulează munca de nivel superior, apoi anulează fiecare referință și o așteaptă într-un bloc finally

async def main():
    sender = asyncio.create_task(uplink())
    watcher = asyncio.create_task(button_watcher())
    try:
        await snapshot_loop()
    finally:
        sender.cancel()
        watcher.cancel()
        await asyncio.gather(sender, watcher,
                             return_exceptions=True)

return_exceptions=True este trucul care împiedică gather-ul să reridice CancelledError pe care fiecare sarcină-copil este pe cale să o livreze, astfel încât motivul de ieșire propriu al aplicației – orice a ridicat sau nu a ridicat snapshot_loop – să fie ceea ce iese din main.