8.15. Zamke#

Isti obrasci koji čine asyncio ugodnim – bez preuzimanja kontrole, eksplicitni awaitovi – daju mu vlastiti skup oblika koji ujedaju. Ova stranica je katalog onih koji se javljaju dovoljno često da ih vrijedi poznavati.

8.15.1. Zaboravljanje await#

Pozivanje funkcije async def vraća objekt korutine. Ono ne pokreće tijelo funkcije. Da bi se zapravo izvela, korutina se mora awaitati ili omotati u zadatak:

async def main():
    send_request()              # bug: returns the coroutine, does nothing
    await send_request()        # right: run it to completion
    asyncio.create_task(send_request())  # right: run it concurrently

Bug je tih – objekt korutine se stvara, odbacuje i nikada ne izvršava. Aplikacija nastavlja kao da je sve uspjelo. MicroPython će ponekad zabilježiti upozorenje da korutina nikada nije počekana; ponekad neće. Pregledajte ima li nedostajućih awaitova na svakom mjestu poziva koje izgleda kao poziv funkcije.

8.15.2. Tijesne petlje bez await#

Korutina koja se izvodi u petlji i nikada ne awaita monopolizira petlju događaja. Nijedan drugi zadatak ne napreduje dok petlja ne izađe ili ne prepusti kontrolu:

async def counter():
    n = 0
    while True:
        n += 1               # bug: starves the loop

Rješenje je prepuštanje kontrole unutar petlje – obično await asyncio.sleep_ms(0) – tako da ostali spremni zadaci dobiju priliku za izvođenje. Računski zahtjevan posao također pripada unutar tog oblika: petlja za obradu slike koja se izvodi stotinama milisekundi po iteraciji trebala bi prepustiti kontrolu barem jednom po iteraciji kako se ostatak programa ne bi zaustavio.

8.15.3. Gutanje CancelledError#

Stranica o otkazivanju to je već detaljno pokrila. Ponavljamo to ovdje jer je to najčešći uzrok problema „moja se aplikacija ne želi ugasiti”: korutina hvata asyncio.CancelledError radi čišćenja i zaboravi je ponovno podići. Zadatak nastavlja s radom; pozivatelj koji je zatražio otkazivanje zauvijek visi čekajući da završi. Uvijek ponovno podignite iznimku nakon čišćenja ili koristite blok try/finally umjesto eksplicitnog except.

8.15.4. Mijenjanje dijeljenog stanja preko await-ova#

Kooperativno raspoređivanje jamči da korutina ima CPU samo za sebe između await-ova, ali čim awaita, druga se korutina može izvoditi. Ako dvije korutine mijenjaju istu strukturu podataka u koracima koji uključuju await, njihove se operacije mogu isprepletati na načine koji oštećuju strukturu:

# bug: two tasks running do_work simultaneously can
# interleave around the await and corrupt items
async def do_work():
    n = len(items)
    await asyncio.sleep_ms(0)
    items.append(some_work(n))

Za stanje koje se mijenja samo između await-ova unutar jedne korutine nije potrebna nikakva sinkronizacija. Za stanje koje se mijenja preko await-ova i kojem se pristupa iz više od jedne korutine, omotajte kritični odsječak u Lock.

8.15.5. await na razini modula#

await je valjan samo unutar tijela async def. Pisanje na razini modula – izvan bilo koje korutine – je sintaksna pogreška:

# bug: not inside an async def
result = await fetch()

Rješenje je staviti posao u korutinu i pozvati je iz ulazne točke programa asyncio.run().

8.15.6. Višestruki pozivi asyncio.run#

MicroPython ima jednu petlju događaja. Pozivanje asyncio.run() dvaput zaredom – jednom za postavljanje, jednom za glavni posao – i dalje koristi istu petlju. Pozivanje iznutra korutine koja se izvodi je pogreška: petlja se već izvodi. Oba se slučaja najčešće javljaju kada skripta organski raste i autor je pokušava proširiti dodavanjem više poziva run() umjesto uklapanja novog posla u postojeći main.

8.15.7. Korištenje Event iz prekida#

asyncio.Event.set() je sigurno pozvati samo iznutra petlje događaja. Pozivanje iz rukovatelja GPIO prekida predstavlja opasnost od oštećenja. Za buđenje zadatka iz prekida koristite ThreadSafeFlagstranica o njemu pokriva taj oblik.

8.15.8. Dugi sinkroni pozivi#

Korutina može počekati asyncio-vlastite primitive za čekanje; sve ostalo što pozove izvodi se sinkrono i blokira petlju dok se ne vrati. Blokirajući time.sleep() od 200 ms, zapis na SD karticu kojem treba 80 ms za pražnjenje, velika JPEG kompresija, poziv csi.CSI.snapshot() – svaki od njih drži petlju događaja punu svoju trajanju. Rješenje ovisi o pozivu:

  • Za time.sleep: zamijenite ga s await asyncio.sleep ili await asyncio.sleep_ms.

  • Za csi.CSI.snapshot: koristite asinkroni omotač za snimanje koji gradi stranica o snimanju.

  • Za dugotrajno računanje (obrada slike, JPEG kodiranje): prihvatite trošak ili razbijte posao na dijelove koji awaitaju između iteracija.

Asyncio ne može učiniti sinkroni poziv neblokirajućim. Može samo dopustiti drugim korutinama da se izvode dok nešto drugo čeka.