8.15. Capcane

Aceleași tipare care fac asyncio plăcut – fără preempțiune, cu await-uri explicite – îi conferă propriul set de forme care mușcă. Această pagină este catalogul celor care apar suficient de des încât să merite să fie cunoscute.

8.15.1. Uitarea unui await

Apelarea unei funcții async def returnează un obiect corutină. Nu rulează corpul funcției. Pentru a o executa efectiv, corutina trebuie să fie așteptată cu await sau încapsulată într-o sarcină:

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

Eroarea este silențioasă – obiectul corutină este creat, abandonat și niciodată executat. Aplicația continuă ca și cum totul ar fi funcționat. MicroPython va jurnaliza uneori un avertisment că o corutină nu a fost niciodată așteptată; alteori nu o va face. Verifică lipsa await-urilor la fiecare loc de apel care arată ca un apel de funcție.

8.15.2. Bucle strânse fără await

O corutină care rulează într-o buclă și nu așteaptă niciodată cu await monopolizează bucla de evenimente. Nicio altă sarcină nu face progrese până când bucla nu iese sau nu cedează controlul:

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

Soluția este o cedare (yield) în interiorul buclei – de obicei await asyncio.sleep_ms(0) – astfel încât alte sarcini gata să ruleze să aibă o șansă. Munca intens computațională ține și ea de această formă: o buclă de procesare a imaginilor care rulează sute de milisecunde per iterație ar trebui să cedeze controlul cel puțin o dată per iterație, astfel încât restul programului să nu se blocheze.

8.15.3. Înghițirea CancelledError

Pagina despre anulare a acoperit deja acest lucru în detaliu. Îl repetăm aici deoarece este cea mai frecventă cauză a problemei „aplicația mea nu se închide”: o corutină prinde asyncio.CancelledError în scopuri de curățare și uită să o ridice din nou. Sarcina continuă să ruleze; apelantul care a cerut anularea așteaptă la nesfârșit ca ea să se termine. Ridică întotdeauna din nou excepția după curățare sau folosește un bloc try/finally în loc de un except explicit.

8.15.4. Modificarea stării partajate între așteptări

Planificarea cooperativă garantează că o corutină are CPU-ul doar pentru ea între așteptări, dar imediat ce așteaptă cu await, o altă corutină poate rula. Dacă două corutine modifică aceeași structură de date în pași care includ await, operațiile lor se pot întrețese în moduri care corup structura:

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

Pentru o stare care este modificată doar între așteptări în interiorul unei singure corutine, nu este necesară nicio sincronizare. Pentru o stare modificată de-a lungul așteptărilor și accesată din mai multe corutine, încapsulează secțiunea critică într-un Lock.

8.15.5. await la nivel de modul

await este valid doar în interiorul unui corp async def. Scrierea lui la nivel de modul – în afara oricărei corutine – este o eroare de sintaxă:

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

Soluția este să pui munca într-o corutină și să o apelezi din punctul de intrare asyncio.run() al programului.

8.15.6. Apeluri multiple asyncio.run

MicroPython are o singură buclă de evenimente. Apelarea asyncio.run() de două ori la rând – o dată pentru configurare, o dată pentru munca principală – folosește în continuare aceeași buclă. Apelarea ei din interiorul unei corutine care rulează este o eroare: bucla rulează deja. Ambele cazuri apar cel mai des atunci când un script crește organic, iar autorul încearcă să-l extindă adăugând mai multe apeluri run() în loc să integreze noua muncă în main-ul existent.

8.15.7. Utilizarea Event dintr-o întrerupere

asyncio.Event.set() poate fi apelată în siguranță doar din interiorul buclei de evenimente. Apelarea ei dintr-un gestionar de întreruperi GPIO reprezintă un pericol de corupere. Pentru a trezi o sarcină dintr-o întrerupere, folosește în schimb ThreadSafeFlagpagina despre ea acoperă forma.

8.15.8. Apeluri sincrone lungi

O corutină poate aștepta propriile primitive de așteptare ale asyncio; orice altceva apelează rulează sincron și blochează bucla până când returnează. Un time.sleep() blocant de 200 ms, o scriere pe cardul SD care durează 80 ms pentru a fi golită, o compresie JPEG mare, un apel csi.CSI.snapshot() – fiecare dintre acestea ține bucla de evenimente pe toată durata sa. Soluția depinde de apel:

  • Pentru time.sleep: înlocuiește-l cu await asyncio.sleep sau await asyncio.sleep_ms.

  • Pentru csi.CSI.snapshot: folosește învelișul de instantaneu asincron construit de pagina de capturare.

  • Pentru calcule lungi (procesare de imagini, codare JPEG): acceptă costul sau împarte munca în bucăți care așteaptă cu await între iterații.

Asyncio nu poate face un apel sincron neblocant. El poate doar să lase alte corutine să ruleze în timp ce altceva este în așteptare.