8.1. Yhteistoiminnallinen rinnakkaisuus

Asyncion ajoitusmalli on yhteistoiminnallinen (cooperative), ei ennakoiva (preemptive). Tämä ero on yksittäisistä ajatusmalleista tärkein, jonka päälle muu osa tästä osiosta rakentuu, joten se kannattaa selventää ennen kuin mitään koodia ilmestyy.

8.1.1. Ennakoiva vs. yhteistoiminnallinen

Ennakoiva ajoitin – sellainen, jota työpöytäkäyttöjärjestelmä käyttää pitääkseen monta ohjelmaa käynnissä yhtä aikaa – voi keskeyttää sen koodinpätkän, joka on parhaillaan käynnissä, millä tahansa hetkellä ja vaihtaa toiseen. Käynnissä olevan koodin ei tarvitse tehdä mitään erityistä; ajoitin keskeyttää sen. Tämä tekee ennakoivasta ajoituksesta erittäin joustavaa (mikään yksittäinen koodinpätkä ei voi nälkiinnyttää muita olemalla hidas), mutta se tarkoittaa myös, että mitä tahansa jaettua muuttujaa on suojattava huolellisesti, koska vaihto voi osua minne tahansa – jopa kesken arvon kirjoittamisen tai kesken listan lukemisen.

Yhteistoiminnallinen ajoitin voi vaihtaa koodinpätkien välillä vain niissä kohdissa, joissa parhaillaan käynnissä oleva pätkä eksplisiittisesti luovuttaa hallinnan takaisin. Asynciossa nuo kohdat ovat jokainen await ja jokainen kutsu sellaiseen korutiiniin, joka luovuttaa hallinnan sisäisesti (yleisimmin asyncio.sleep()). Kahden awaitin välissä käynnissä oleva korutiini saa suorittimen yksin käyttöönsä.

Tästä seuraa kaksi seurausta:

  • Korutiini, joka ei koskaan awaitaa, ei koskaan pysähdy. Jos korutiini jää tiukkaan silmukkaan ilman await -kohtaa sisällään, se monopolisoi ajoittimen eikä mikään muu etene. Korjaus on await asyncio.sleep_ms(0) (tai jokin muu odotuskutsu) järkevässä kohdassa silmukkaa.

  • Jaettu tila on turvallista awaitien välissä. Kaksi korutiinia eivät voi limittyä kesken operaation, jossa ei ole await -kohtaa. Sellaista vioittumista, joka syntyy kun ennakointi osuu kesken monivaiheisen päivityksen – yksi koodinpätkä lukee arvoa samalla kun toinen on kesken sen muuttamista – ei yksinkertaisesti voi tapahtua täällä. Korutiinien välistä koordinointia tarvitaan silti, kun useamman niistä on jaettava resurssi awaitien yli, mutta kesken-rivin-limittymisen ongelma ei päde.

8.1.2. Kolme kerrosta

Jokainen asyncio-skripti rakentuu samoista kolmesta kerroksesta. Seuraavat kaksi sivua käsittelevät niitä yksityiskohtaisesti; nämä ovat ne nimitykset, jotka kannattaa pitää mielessä niitä lukiessa.

  • Korutiinit (Coroutines) – funktiot, jotka on määritelty async def -määreellä, kukin itsenäinen työyksikkö, joka awaitaa siellä missä on tarkoituksenmukaista. Python Overview esitteli async/await -avainsanat; asynciossa ne ovat se tapa, jolla korutiini luovuttaa hallinnan takaisin ajoittimelle.

  • Tehtävät (Tasks) – kääre, jonka asyncio.create_task() laittaa korutiinin ympärille ajoittaakseen sen rinnakkain nykyisen kanssa. Sovellus luo tyypillisesti kourallisen tehtäviä pitkäkestoisia töitä varten (tilannekuvasilmukka, verkkoasiakas, UART-lukija, …).

  • Tapahtumasilmukka (event loop) – alla oleva moottori, joka pitää kirjaa siitä, mitkä korutiinit odottavat ja mitkä ovat valmiita suoritettaviksi, vaihtaen tehtävien välillä jokaisessa await -kohdassa. Sovellus ei kirjoita silmukkaa; se luovuttaa ylimmän tason korutiinin asyncio.run() -funktiolle ja silmukka ohjaa kaikkea sieltä käsin.

Kun sovellus kuvataan tällä tavalla – pienenä joukkona korutiineja, jotka tapahtumasilmukka kokoaa yhteen – rinnakkaisuudesta tulee ohjelman muodon ominaisuus, ei jotain, mitä sovelluksen pitäisi hallita vaihe vaiheelta.