8.15. Buktatók

Ugyanazok a minták, amelyek az asyncio-t kellemessé teszik – nincs előzetes kiszorítás, explicit await-ek –, megadják a saját, harapós formáit. Ez az oldal azoknak a katalógusa, amelyek elég gyakran felmerülnek ahhoz, hogy érdemes legyen tudni róluk.

8.15.1. Az await elfelejtése

Egy async def függvény meghívása egy korutinobjektumot ad vissza. Nem futtatja le a függvény törzsét. A tényleges végrehajtáshoz a korutint await-elni kell, vagy egy feladatba kell csomagolni:

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

A hiba néma – a korutinobjektum létrejön, eldobódik, és soha nem fut le. Az alkalmazás úgy halad tovább, mintha minden működött volna. A MicroPython néha figyelmeztetést naplóz arról, hogy egy korutint soha nem vártak be; néha nem. Ellenőrizd a hiányzó await-eket minden olyan hívási helyen, amely függvényhívásnak tűnik.

8.15.2. Szoros ciklusok await nélkül

Egy korutin, amely ciklusban fut, és soha nem await-el, kisajátítja az eseményhurkot. Egyetlen más feladat sem halad előre, amíg a ciklus ki nem lép vagy át nem adja a vezérlést:

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

A megoldás egy yield a cikluson belül – általában await asyncio.sleep_ms(0) –, hogy más, futásra kész feladatok is esélyt kapjanak a futásra. A számításigényes munka is ebbe a formába tartozik: egy képfeldolgozó ciklusnak, amely iterációnként több száz milliszekundumig fut, iterációnként legalább egyszer át kell adnia a vezérlést, hogy a program többi része ne akadjon el.

8.15.3. A CancelledError elnyelése

A megszakítás oldal ezt már részletesen tárgyalta. Azért ismételjük meg itt, mert ez a leggyakoribb oka annak, hogy „az alkalmazásom nem áll le”: egy korutin elkapja az asyncio.CancelledError-t takarítás céljából, és elfelejti újra kiváltani. A feladat tovább fut; a hívó, amely a megszakítást kérte, örökre vár arra, hogy befejeződjön. A takarítás után mindig váltsd ki újra, vagy explicit except helyett használj try/finally blokkot.

8.15.4. Megosztott állapot módosítása await-ek között

A kooperatív ütemezés garantálja, hogy egy korutiné a CPU az await-ek között, de amint await-el, egy másik korutin futhat. Ha két korutin ugyanazt az adatstruktúrát módosítja olyan lépésekben, amelyek await-et tartalmaznak, a műveleteik úgy fonódhatnak össze, hogy az megrongálja a struktúrát:

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

Olyan állapotnál, amelyet csak egy korutinon belül, await-ek között módosítanak, nincs szükség szinkronizációra. Olyan állapotnál, amelyet await-eken átívelően módosítanak, és egynél több korutinból érnek el, a kritikus szakaszt egy Lock-ba kell csomagolni.

8.15.5. Modulszintű await

Az await csak egy async def törzsén belül érvényes. Modulszinten való megírása – bármely korutinon kívül – szintaktikai hiba:

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

A megoldás az, hogy a munkát egy korutinba helyezed, és a program asyncio.run() belépési pontjából hívod meg.

8.15.6. Több asyncio.run hívás

A MicroPythonnak egy eseményhurka van. Az asyncio.run() kétszeri egymás utáni meghívása – egyszer a beállításhoz, egyszer a fő munkához – továbbra is ugyanazt a hurkot használja. Egy futó korutinon belülről való meghívása hiba: a hurok már fut. Mindkét eset leggyakrabban akkor merül fel, amikor egy szkript szervesen növekszik, és a szerző úgy próbálja bővíteni, hogy további run() hívásokat ad hozzá, ahelyett hogy az új munkát beleolvasztaná a meglévő main-be.

8.15.7. Az Event használata megszakításból

Az asyncio.Event.set() csak az eseményhurkon belülről hívható biztonságosan. Egy GPIO-megszakításkezelőből való meghívása korrupciós kockázat. Egy feladat megszakításból való felébresztéséhez használd inkább az ThreadSafeFlag-et – az erről szóló oldal ismerteti a formát.

8.15.8. Hosszú szinkron hívások

Egy korutin bevárhatja az asyncio saját várakozási primitívjeit; bármi más, amit meghív, szinkronban fut, és blokkolja a hurkot, amíg vissza nem tér. Egy 200 ms-os blokkoló time.sleep(), egy SD-kártya-írás, amelynek 80 ms-ig tart a kiürítése, egy nagy JPEG-tömörítés, egy csi.CSI.snapshot() hívás – ezek mindegyike a teljes időtartamára lefoglalja az eseményhurkot. A megoldás a hívástól függ:

  • A time.sleep esetén: cseréld ki await asyncio.sleep-re vagy await asyncio.sleep_ms-re.

  • A csi.CSI.snapshot esetén: használd az aszinkron snapshot-burkolót, amelyet a rögzítési oldal felépít.

  • Hosszú számításhoz (képfeldolgozás, JPEG-kódolás): fogadd el a költséget, vagy bontsd a munkát olyan részekre, amelyek az iterációk között await-elnek.

Az asyncio nem tudja nem blokkolóvá tenni a szinkron hívásokat. Csak annyit tehet, hogy más korutinokat futtat, miközben valami más vár.