8.1. Kooperatív konkurencia

Az asyncio ütemezési modellje kooperatív, nem preemptív. Ez a megkülönböztetés a legfontosabb gondolati modell, amelyre a fejezet többi része épül, ezért érdemes tisztázni, mielőtt bármilyen kód megjelenne.

8.1.1. Preemptív kontra kooperatív

Egy preemptív ütemező — amilyet egy asztali operációs rendszer használ, hogy egyszerre sok programot futtasson — bármelyik pillanatban szüneteltetheti az éppen futó kódot, és átválthat egy másikra. A futó kódnak nem kell semmi különöset tennie; az ütemező megszakítja. Ez nagyon rugalmassá teszi a preemptív ütemezést (egyetlen kódrészlet sem éheztetheti ki a többit azzal, hogy lassú), de azt is jelenti, hogy minden megosztott változót gondosan védeni kell, mert a váltás bárhol bekövetkezhet — akár egy érték írásának felénél, akár egy lista olvasásának közepén.

Egy kooperatív ütemező csak olyan pontokon válthat a kódrészletek között, ahol az éppen futó részlet kifejezetten visszaadja a vezérlést. Az asyncio esetében ezek a pontok minden await, és minden olyan korutin hívása, amely belsőleg ad át vezérlést (leggyakrabban az asyncio.sleep()). Két await között a futó korutiné a CPU.

Ebből két következmény adódik:

  • Egy korutin, amely soha nem await-el, soha nem áll meg. Ha egy korutin egy szoros ciklusban marad await nélkül, akkor lefoglalja az ütemezőt, és semmi más nem halad előre. A megoldás az, hogy a ciklus egy értelmes pontján meghívunk egy await asyncio.sleep_ms(0) (vagy más várakozó) hívást.

  • A megosztott állapot biztonságos az await-ek között. Két korutin nem fonódhat össze egy olyan művelet felénél, amelyben nincs await. Az a fajta sérülés, amely akkor merül fel, amikor a preempció egy többlépéses frissítés közepén csap le — az egyik kódrészlet egy értéket olvas, miközben egy másik éppen a felénél tart a megváltoztatásnak — itt egyszerűen nem fordulhat elő. A korutinok közötti összehangolásra továbbra is szükség van, amikor többüknek meg kell osztaniuk egy erőforrást az await-eken átívelően, de a sor közepén történő összefonódás problémája itt nem áll fenn.

8.1.2. A három réteg

Minden asyncio szkript ugyanabból a három rétegből épül fel. A következő két oldal részletesen tárgyalja őket; ezek a címkék érdemes szem előtt tartani olvasás közben.

  • Korutinokasync def-fel deklarált függvények, amelyek mindegyike egy önálló munkaegység, és ott await-el, ahol szükséges. A Python áttekintő bemutatta az async/await kulcsszavakat; az asyncio-ban ezek révén ad vissza vezérlést egy korutin az ütemezőnek.

  • Feladatok — egy burkoló, amelyet az asyncio.create_task() helyez egy korutin köré, hogy az aktuálissal párhuzamosan ütemezze. Az alkalmazás jellemzően néhány feladatot hoz létre a hosszan futó munkákhoz (a pillanatkép-ciklus, a hálózati kliens, a UART-olvasó, …).

  • Az eseményhurok — a motor a háttérben, amely nyilvántartja, hogy mely korutinok várakoznak és melyek készek a futásra, minden await-nél váltva a feladatok között. Az alkalmazás nem írja meg a hurkot; átad egy felső szintű korutint az asyncio.run() függvénynek, és a hurok onnantól mindent mozgat.

Amikor az alkalmazást így írjuk le — mint egy kis korutinhalmazt, amelyet egy eseményhurok fűz össze —, a konkurencia a program alakjának tulajdonságává válik, nem pedig olyasmivé, amit az alkalmazásnak lépésről lépésre kell kezelnie.