8.2. Coroutines et tâches¶
Les coroutines sont l’unité de travail à partir de laquelle un programme asyncio est bâti ; les tâches sont la manière dont une application exécute plusieurs coroutines en parallèle.
8.2.1. Coroutines¶
Une coroutine est une fonction déclarée avec async def:
import asyncio
async def heartbeat(interval_ms):
while True:
print("tick")
await asyncio.sleep_ms(interval_ms)
Le corps ressemble à une fonction ordinaire, avec un ingrédient supplémentaire : await. Partout où la coroutine doit attendre quelque chose — une pause, une lecture réseau, un événement déclenché — elle attend (await) une expression qui sait comment suspendre la coroutine jusqu’à ce que la chose attendue soit prête. À chaque await, la coroutine rend le contrôle à asyncio ; asyncio la reprend au même point une fois que l’opération attendue est terminée.
Le module asyncio fournit deux fonctions de pause :
asyncio.sleep()— argument en secondes, accepte un float.asyncio.sleep_ms()— argument en millisecondes, prend un int. Une extension MicroPython ; généralement le bon choix sur la caméra car les réglages temporels du micrologiciel sont exprimés en millisecondes.
Un simple async def ne fait rien en lui-même. Appeler heartbeat(500) n’exécute pas le corps ; cela renvoie un objet coroutine qu’asyncio doit ordonnancer. La manière la plus simple d’en ordonnancer un est asyncio.run():
asyncio.run(heartbeat(500))
asyncio.run() démarre la boucle d’événements, ordonnance la coroutine qui lui a été remise comme point d’entrée de premier niveau, pilote la boucle jusqu’à ce que cette coroutine retourne, puis démonte la boucle. Pour une coroutine unique, c’est tout le programme. Pour plusieurs coroutines, l’application a recours aux tâches.
8.2.2. Tâches¶
Une tâche est l’emballage d’asyncio autour d’une coroutine qui dit ordonnance celle-ci en parallèle de la coroutine courante et laisse-moi continuer. asyncio.create_task() en crée une et renvoie un objet Task représentant le travail ordonnancé:
task = asyncio.create_task(heartbeat("fast", 100))
La coroutine est désormais dans l’ordonnancement de la boucle ; l’appelant ne l’a pas attendue. Le Task renvoyé est la poignée que l’appelant utilise ensuite pour interagir avec ce travail en cours d’exécution.
Une fois que l’application dispose de la poignée, elle peut en faire trois choses :
Attendre la fin de la tâche. Un
Taskest lui-même attendable (awaitable).result = await tasksuspend la coroutine courante jusqu’à ce que la coroutine detaskretourne, puis reprend avec ce que cette coroutine a renvoyé (ou relève à nouveau ce qu’elle a levé).Annuler la tâche.
task.cancel()programme la levée deasyncio.CancelledErrorà l’intérieur de la coroutine de la tâche à son prochainawait, lui donnant la possibilité d’exécuter du code de nettoyage dans un blocfinally. La page sur les délais d’attente et l’annulation en couvre les détails.L’identifier plus tard.
asyncio.current_task()renvoie leTaskde la coroutine en cours d’exécution. La plupart des scripts ne l’appellent jamais ; il apparaît dans l’instrumentation et dans les gestionnaires d’exceptions.
Le script n’a pas besoin de capturer la poignée à chaque fois. Les tâches d’arrière-plan jetables que l’application démarre et laisse tourner peuvent abandonner la valeur de retour — la boucle les ordonnance quand même:
import asyncio
async def heartbeat(name, interval_ms):
while True:
print(name)
await asyncio.sleep_ms(interval_ms)
async def main():
asyncio.create_task(heartbeat("fast", 100))
asyncio.create_task(heartbeat("slow", 500))
await asyncio.sleep(5)
asyncio.run(main())
Les deux appels à create_task ordonnancent les deux heartbeats sans attendre ni l’un ni l’autre. Le contrôle revient immédiatement à main, qui attend (await) ensuite une pause de cinq secondes. Pendant qu’elle dort, les deux tâches heartbeat progressent ; la boucle parcourt en boucle la tâche qui est prête à s’exécuter. Au bout de cinq secondes, main retourne, la boucle démonte les tâches encore en vie, et asyncio.run() retourne à l’appelant.
Capturez la poignée chaque fois que l’application a réellement besoin de l’une des trois opérations ci-dessus. En pratique, cela signifie presque toujours, car arrêter proprement une application implique d’annuler les tâches d’arrière-plan qu’elle a engendrées — la page sur l’annulation couvre ce schéma.
8.2.3. La règle des deux lignes¶
Le programme asyncio minimal, ce sont les deux lignes par lesquelles les exemples ci-dessus se terminent:
async def main():
...
asyncio.run(main())
Tout le reste — les tâches que l’application crée, les primitives avec lesquelles elle les coordonne, les flux qu’elle ouvre — se produit à l’intérieur de main (et à l’intérieur des coroutines que main engendre). Lorsqu’un script dépasse la boucle classique while True: csi0.snapshot() de la caméra, la réponse n’est pas d’appeler asyncio.run() à plusieurs endroits ; c’est d’intégrer le nouveau travail dans main sous forme de tâches supplémentaires.