8.1. Kooperativ samtidighet¶
Asyncios schemaläggningsmodell är kooperativ, inte preemptiv. Denna skillnad är den enskilt viktigaste mentala modell som resten av detta avsnitt bygger på, så det är värt att fastställa den innan någon kod dyker upp.
8.1.1. Preemptiv kontra kooperativ¶
En preemptiv schemaläggare – den sort som ett skrivbordsoperativsystem använder för att hålla många program igång samtidigt – kan när som helst pausa den kod som för tillfället körs och växla till en annan. Den körande koden behöver inte göra något särskilt; schemaläggaren avbryter den. Det gör preemptiv schemaläggning mycket flexibel (ingen enskild kodbit kan svälta ut de andra genom att vara långsam), men det innebär också att varje delad variabel måste skyddas noggrant, eftersom växlingen kan landa var som helst – till och med mitt under skrivningen av ett värde, eller halvvägs genom läsningen av en lista.
En kooperativ schemaläggare kan endast växla mellan kodbitar vid punkter där den för tillfället körande biten uttryckligen lämnar tillbaka kontrollen. I asyncio är dessa punkter varje await och varje anrop till en coroutine som internt ger efter (vanligast asyncio.sleep()). Mellan två await:er har den körande coroutinen processorn för sig själv.
Två konsekvenser följer av detta:
En coroutine som aldrig await:ar blir aldrig pausad. Om en coroutine sitter i en tät loop utan något
awaitinuti monopoliserar den schemaläggaren och inget annat kommer framåt. Lösningen är attawait asyncio.sleep_ms(0)(eller något annat väntande anrop) vid en lämplig punkt i loopen.Delat tillstånd är säkert mellan await:er. Två coroutiner kan inte fläta in sig halvvägs genom en operation som inte har något
awaiti sig. Den sorts korruption som uppstår när preemption landar mitt i en flerstegsuppdatering – en kodbit som läser ett värde medan en annan är halvvägs genom att ändra det – kan helt enkelt inte inträffa här. Samordning mellan coroutiner behövs fortfarande när flera av dem måste dela en resurs över await:er, men problemet med inflätning mitt i en rad gäller inte.
8.1.2. De tre lagren¶
Varje asyncio-skript byggs av samma tre lager. De följande två sidorna behandlar dem i detalj; detta är etiketterna att ha i åtanke under läsningen.
Coroutiner – funktioner deklarerade med
async def, var och en en självständig arbetsenhet som await:ar där det är lämpligt. Python-översikten introducerade nyckelordenasync/await; i asyncio är de hur en coroutine ger efter tillbaka till schemaläggaren.Uppgifter (tasks) – en omslagning som
asyncio.create_task()lägger runt en coroutine för att schemalägga den samtidigt med den nuvarande. Applikationen skapar vanligtvis en handfull uppgifter för de långkörande jobben (stillbildsloopen, nätverksklienten, UART-läsaren, …).Händelseloopen – motorn under huven som håller reda på vilka coroutiner som väntar och vilka som är redo att köra, och växlar mellan uppgifter vid varje
await. Applikationen skriver inte loopen; den lämnar en coroutine på toppnivå tillasyncio.run()och loopen driver allt därifrån.
När applikationen beskrivs på det sättet – som en liten uppsättning coroutiner sammansatta av en händelseloop – blir samtidigheten en egenskap hos programmets form, inte något applikationen måste hantera steg för steg.