8.1. Concurență cooperativă

Modelul de planificare al asyncio este cooperativ, nu preemptiv. Această distincție este cel mai important model mental pe care se construiește restul acestei secțiuni, așa că merită clarificată înainte ca vreun cod să apară.

8.1.1. Preemptiv vs cooperativ

Un planificator preemptiv – genul pe care îl folosește un sistem de operare desktop pentru a menține multe programe rulând simultan – poate întrerupe oricare bucată de cod care rulează în prezent în orice moment și poate comuta la alta. Codul care rulează nu trebuie să facă nimic special; planificatorul îl întrerupe. Acest lucru face ca planificarea preemptivă să fie foarte flexibilă (nicio bucată de cod nu poate înfometa celelalte fiind lentă), dar înseamnă, de asemenea, că orice variabilă partajată trebuie apărată cu grijă, deoarece comutarea ar putea avea loc oriunde – chiar și la jumătatea scrierii unei valori sau în mijlocul citirii unei liste.

Un planificator cooperativ poate comuta între bucăți de cod doar în punctele în care bucata care rulează în prezent predă explicit controlul înapoi. În asyncio, acele puncte sunt fiecare await și fiecare apel către o corutină care cedează controlul intern (cel mai frecvent asyncio.sleep()). Între două await-uri, corutina care rulează are CPU-ul doar pentru ea.

De aici decurg două consecințe:

  • O corutină care nu face niciodată await nu este niciodată pusă în pauză. Dacă o corutină stă într-o buclă strânsă fără niciun await înăuntru, monopolizează planificatorul și nimic altceva nu avansează. Soluția este să faci await asyncio.sleep_ms(0) (sau alt apel de așteptare) într-un punct rezonabil al buclei.

  • Starea partajată este sigură între await-uri. Două corutine nu se pot întrepătrunde la jumătatea unei operații care nu conține niciun await. Tipul de coruperea care apare atunci când preempțiunea cade în mijlocul unei actualizări în mai mulți pași – o bucată de cod citind o valoare în timp ce alta este la jumătatea modificării ei – pur și simplu nu se poate întâmpla aici. Coordonarea între corutine este totuși necesară atunci când mai multe dintre ele trebuie să partajeze o resursă de-a lungul await-urilor, dar problema întrepătrunderii la jumătatea unei linii nu se aplică.

8.1.2. Cele trei straturi

Fiecare script asyncio este construit din aceleași trei straturi. Următoarele două pagini le acoperă în detaliu; acestea sunt etichetele de reținut în timpul citirii lor.

  • Corutinele – funcții declarate cu async def, fiecare fiind o unitate de lucru de sine stătătoare care face await acolo unde este cazul. Prezentarea generală Python a introdus cuvintele-cheie async/await; în asyncio, acestea sunt modul în care o corutină cedează controlul înapoi planificatorului.

  • Sarcinile – un wrapper pe care asyncio.create_task() îl pune în jurul unei corutine pentru a o planifica concurent cu cea curentă. Aplicația creează de obicei câteva sarcini pentru lucrările de lungă durată (bucla de instantanee, clientul de rețea, cititorul UART, …).

  • Bucla de evenimente – motorul de dedesubt care urmărește care corutine așteaptă și care sunt gata să ruleze, comutând între sarcini la fiecare await. Aplicația nu scrie bucla; ea predă o corutină de nivel superior către asyncio.run(), iar bucla conduce totul de acolo.

Când aplicația este descrisă în acest fel – ca un mic set de corutine compuse de o buclă de evenimente – concurența devine o proprietate a formei programului, nu ceva ce aplicația trebuie să gestioneze pas cu pas.