2.32. Coroutines

Een coroutine is een functie die halverwege kan pauzeren en later kan hervatten vanaf waar ze was gebleven, met al haar lokale variabelen intact. Het weerspiegelt het generatorpatroon – een functielichaam dat kan worden onderbroken en hervat – met één verandering in wie elke hervatting aandrijft.

Python’s syntaxis voor het schrijven van een coroutine is het sleutelwoordpaar async / await. async markeert de functie als coroutine; await markeert de punten erbinnen waar pauzeren is toegestaan.

2.32.1. Een coroutine definiëren

Een functie die met async def is gedefinieerd, is een coroutinefunctie. Het aanroepen ervan voert het lichaam niet uit; het retourneert een coroutine-object dat nog niet is gestart:

async def greet(name):
    print("hello,", name)

coro = greet("Alice")        # body NOT run yet

Het coroutine-object is gepauzeerd aan het allereerste begin van de functie. Iets moet het aandrijven om het lichaam te laten draaien – dat iets is een event loop, een runtime-component. MicroPython levert er een in de asyncio-module. Beschouw de coroutine voorlopig als “klaar om te draaien, wachtend op een aandrijver”.

2.32.2. Pauzeren binnen een coroutine

Binnen een coroutine onderbreekt een await-expressie de uitvoering totdat de awaited waarde gereed is:

async def fetch_and_log():
    data = await read_sensor()
    print("got:", data)

Wanneer het lichaam await read_sensor() bereikt, geeft de coroutine de controle terug aan wat het ook uitvoert. Wanneer read_sensor() klaar is, hervat de aandrijver de coroutine op de volgende regel, met het resultaat gebonden aan data.

await is alleen geldig binnen een coroutine. Het gebruiken in een gewone functie is een syntaxisfout.

2.32.3. Relatie met generators

Coroutines en generators delen dezelfde onderliggende werking. Het verschil zit in wie elke hervatting trekt:

  • Een generator levert waarden op; de consument haalt de volgende op met next() of door te itereren.

  • Een coroutine levert controle op; een event loop plant de hervatting wanneer de awaited bewerking gereed is.

Als de generator-yield-handdruk logisch is, dan is de coroutine-handdruk hetzelfde idee – alleen aangedreven door een event loop in plaats van een for-lus.

Een event loop is een kleine dispatcher die een lijst bijhoudt van coroutines die op iets wachten (een timer, een netwerkgebeurtenis, een andere coroutine die klaar is). Bij elke iteratie kiest hij een coroutine waarvan de wachttijd is voldaan, hervat hem tot de volgende await, noteert vervolgens waar die coroutine nu op wacht en gaat verder naar een andere gerede coroutine. Het resultaat is dat veel taken gelijktijdig voortgang boeken op één enkele thread – elke coroutine geeft vrijwillig de controle op bij zijn await-punten, en de lus vult die momenten met welke andere coroutines er ook maar klaar zijn om voortgang te boeken.

Onder de motorkap gebruiken await en yield dezelfde Python-runtimefunctie voor het onderbreken en hervatten van een functie. De sleutelwoorden verschillen omdat de conventie eromheen verschilt: yield geeft een waarde terug aan een consument die met next() ophaalt; await geeft de controle aan een event loop die de hervatting plant wanneer de awaited bewerking gereed is. async / await is in wezen nieuwere syntaxis voor het coroutinepatroon – oudere bibliotheken bouwden coroutines rechtstreeks bovenop de generatormachinerie, met yield from (geïntroduceerd in Iterators en generators) om onderbreking tussen coroutines te delegeren.

2.32.4. Coroutines hebben een aandrijver nodig

Een coroutine is inert zonder een runtime om hem aan te drijven. Er een definiëren is prima; er een uitvoeren vereist een event loop. MicroPython’s asyncio-module biedt die event loop. De sectie Asyncio behandelt hoe je de lus start, coroutines erop plant, toestand tussen ze deelt met locks en events, annulering en time-outs afhandelt, en een echte applicatie vormgeeft rond de hier geïntroduceerde sleutelwoorden async / await.