8.15. Sudenkuopat

Samat mallit, jotka tekevät asyncio:sta mukavan – ei esivaltaa, eksplisiittiset awaitt – antavat sille oman joukon hankalia muotoja. Tämä sivu on luettelo niistä, jotka tulevat vastaan riittävän usein, jotta niitä kannattaa tuntea.

8.15.1. await-lauseen unohtaminen

async def -funktion kutsuminen palauttaa korutiiniobjektin. Se ei suorita funktion runkoa. Sen tosiasialliseksi suorittamiseksi korutiinia on awaittava tai se on käärittävä tehtävään:

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

Vika on hiljainen – korutiiniobjekti luodaan, hylätään eikä koskaan suoriteta. Sovellus etenee kuin kaikki olisi toiminut. MicroPython kirjaa joskus varoituksen siitä, että korutiinia ei koskaan odotettu; joskus se ei tee niin. Tarkista puuttuvien awaitien varalta jokainen kutsupaikka, joka näyttää funktiokutsulta.

8.15.2. Tiiviit silmukat ilman await-lausetta

Korutiini, joka pyörii silmukassa eikä koskaan awaittaa, monopolisoi tapahtumasilmukan. Mikään muu tehtävä ei etene, ennen kuin silmukka poistuu tai luovuttaa vuoron:

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

Korjaus on yield silmukan sisällä – yleensä await asyncio.sleep_ms(0) – jotta muut valmiit tehtävät saavat mahdollisuuden suorittua. Laskentaintensiivinen työ kuuluu myös tähän muotoon: kuvankäsittelysilmukan, joka kestää satoja millisekunteja iteraatiota kohden, tulisi luovuttaa vuoro vähintään kerran iteraatiossa, jotta muu ohjelma ei pysähdy.

8.15.3. CancelledError-poikkeuksen nieleminen

peruutussivu käsitteli tämän jo yksityiskohtaisesti. Toistamme sen tässä, koska se on yleisin syy siihen, että ”sovellukseni ei sammu”: korutiini nappaa asyncio.CancelledError -poikkeuksen siivousta varten ja unohtaa nostaa sen uudelleen. Tehtävä jatkaa suoritustaan; kutsuja, joka pyysi peruutusta, jää ikuisesti odottamaan sen valmistumista. Nosta aina uudelleen siivouksen jälkeen tai käytä try/finally -lohkoa eksplisiittisen exceptin sijaan.

8.15.4. Jaetun tilan muuttaminen await-lauseiden yli

Yhteistoiminnallinen ajastus takaa, että korutiinilla on suoritin yksin itselleen await-lauseiden välillä, mutta heti kun se awaittaa, toinen korutiini voi suorittua. Jos kaksi korutiinia muokkaa samaa tietorakennetta vaiheissa, joihin sisältyy await, niiden toiminnot voivat limittyä tavoilla, jotka turmelevat rakenteen:

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

Tilalle, jota muutetaan vain await-lauseiden välillä yhden korutiinin sisällä, ei tarvita synkronointia. Tilalle, jota muutetaan await-lauseiden yli ja jota käytetään useammasta kuin yhdestä korutiinista, kääri kriittinen osio Lock -lukkoon.

8.15.5. Moduulitason await

await on kelvollinen vain async def -rungon sisällä. Sen kirjoittaminen moduulitasolla – minkään korutiinin ulkopuolella – on syntaksivirhe:

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

Korjaus on sijoittaa työ korutiiniin ja kutsua sitä ohjelman asyncio.run() -aloituspisteestä.

8.15.6. Useat asyncio.run -kutsut

MicroPythonissa on yksi tapahtumasilmukka. asyncio.run() -funktion kutsuminen kahdesti peräkkäin – kerran asennukseen, kerran päätyöhön – käyttää silti samaa silmukkaa. Sen kutsuminen käynnissä olevan korutiinin sisältä on virhe: silmukka on jo käynnissä. Molemmat tapaukset tulevat vastaan useimmiten silloin, kun skripti kasvaa orgaanisesti ja tekijä yrittää laajentaa sitä lisäämällä lisää run() -kutsuja sen sijaan, että taittaisi uuden työn olemassa olevaan mainiin.

8.15.7. Event -luokan käyttö keskeytyksestä

asyncio.Event.set() -metodia on turvallista kutsua vain tapahtumasilmukan sisältä. Sen kutsuminen GPIO-keskeytyskäsittelijästä on turmelusriski. Tehtävän herättämiseen keskeytyksestä käytä sen sijaan ThreadSafeFlag -luokkaa – sitä käsittelevä sivu esittelee muodon.

8.15.8. Pitkät synkroniset kutsut

Korutiini voi odottaa asyncio:n omia odotusprimitiivejä; kaikki muu, mitä se kutsuu, suoritetaan synkronisesti ja estää silmukan, kunnes se palaa. 200 ms:n estävä time.sleep(), SD-korttikirjoitus, jonka tyhjentäminen kestää 80 ms, suuri JPEG-pakkaus, csi.CSI.snapshot() -kutsu – jokainen näistä pitää tapahtumasilmukkaa koko kestonsa ajan. Korjaus riippuu kutsusta:

  • time.sleep -kutsulle: korvaa se await asyncio.sleep - tai await asyncio.sleep_ms -kutsulla.

  • csi.CSI.snapshot -kutsulle: käytä asynkronista snapshot-käärettä, jonka kaappaussivu rakentaa.

  • Pitkälle laskennalle (kuvankäsittely, JPEG-koodaus): hyväksy kustannus tai pilko työ paloihin, jotka awaittavat iteraatioiden välillä.

Asyncio ei voi tehdä synkronisesta kutsusta estämätöntä. Se voi vain antaa muiden korutiinien suorittua sillä aikaa, kun jokin muu odottaa.