8.1. Kooperativna konkurentnost

Asyncio-ov model raspoređivanja je kooperativan, a ne preemptivan. Ta razlika najvažniji je mentalni model na kojem se gradi ostatak ovog odjeljka, pa ju je vrijedno utvrditi prije nego što se pojavi ikakav kod.

8.1.1. Preemptivno naspram kooperativnog

Preemptivni raspoređivač – vrsta kakvu desktop operativni sustav koristi za istovremeno održavanje mnogih programa u radu – može zaustaviti onaj dio koda koji se trenutno izvodi u bilo kojem trenutku i prebaciti se na drugi. Kod koji se izvodi ne mora učiniti ništa posebno; raspoređivač ga prekida. Time je preemptivno raspoređivanje vrlo fleksibilno (nijedan dio koda ne može izgladnjivati ostale time što je spor), ali to ujedno znači da svaka dijeljena varijabla mora biti pomno zaštićena, jer prebacivanje može pasti bilo gdje – čak i usred upisivanja vrijednosti ili na pola čitanja liste.

Kooperativni raspoređivač može se prebacivati između dijelova koda samo na točkama na kojima dio koji se trenutno izvodi eksplicitno vraća kontrolu. U asyncio-u te su točke svaki await i svaki poziv koroutine koja interno predaje kontrolu (najčešće asyncio.sleep()). Između dva awaita koroutina koja se izvodi ima CPU samo za sebe.

Iz toga proizlaze dvije posljedice:

  • Koroutina koja nikad ne awaita nikad neće biti zaustavljena. Ako koroutina sjedi u tijesnoj petlji bez await unutra, monopolizira raspoređivač i ništa drugo ne napreduje. Rješenje je await asyncio.sleep_ms(0) (ili neki drugi poziv koji čeka) na razumnoj točki u petlji.

  • Dijeljeno stanje je sigurno između awaitova. Dvije koroutine ne mogu se ispreplitati na pola operacije koja u sebi nema await. Vrsta oštećenja koja nastaje kad preempcija padne usred višekoračnog ažuriranja – jedan dio koda čita vrijednost dok je drugi na pola njezine promjene – jednostavno se ovdje ne može dogoditi. Koordinacija između koroutina i dalje je potrebna kad njih nekoliko mora dijeliti resurs preko awaitova, ali problem ispreplitanja usred jednog retka ne vrijedi.

8.1.2. Tri sloja

Svaka asyncio skripta izgrađena je od istih triju slojeva. Sljedeće dvije stranice obrađuju ih detaljno; ovo su oznake koje treba imati na umu dok ih čitate.

  • Koroutine – funkcije deklarirane s async def, svaka samostalna jedinica rada koja awaita gdje je prikladno. Pregled Pythona uveo je ključne riječi async/await; u asyncio-u one su način na koji koroutina predaje kontrolu natrag raspoređivaču.

  • Zadaci – omotač koji asyncio.create_task() stavlja oko koroutine kako bi je rasporedio konkurentno s trenutnom. Aplikacija obično stvara nekolicinu zadataka za dugotrajne poslove (petlju snimaka, mrežnog klijenta, UART čitača, …).

  • Petlja događaja – pogon ispod svega koji prati koje koroutine čekaju, a koje su spremne za izvođenje, prebacujući se između zadataka pri svakom await. Aplikacija ne piše petlju; predaje koroutinu najviše razine funkciji asyncio.run(), a petlja odande pogoni sve.

Kad se aplikacija opiše na taj način – kao mali skup koroutina sastavljenih putem petlje događaja – konkurentnost postaje svojstvo oblika programa, a ne nešto čime aplikacija mora upravljati korak po korak.