8.15. Pièges¶
Les mêmes schémas qui rendent asyncio agréable – pas de préemption, des await explicites – lui donnent son propre lot de situations qui mordent. Cette page est le catalogue de celles qui reviennent assez souvent pour qu’il vaille la peine de les connaître.
8.15.1. Oublier d”await¶
Appeler une fonction async def renvoie un objet coroutine. Cela n’exécute pas le corps de la fonction. Pour réellement l’exécuter, la coroutine doit être attendue (await) ou enveloppée dans une tâche:
async def main():
send_request() # bug: returns the coroutine, does nothing
await send_request() # right: run it to completion
asyncio.create_task(send_request()) # right: run it concurrently
Le bogue est silencieux – l’objet coroutine est créé, abandonné et jamais exécuté. L’application poursuit comme si tout avait fonctionné. MicroPython journalisera parfois un avertissement signalant qu’une coroutine n’a jamais été attendue ; parfois non. Vérifiez l’absence de await à chaque site d’appel qui ressemble à un appel de fonction.
8.15.2. Boucles serrées sans await¶
Une coroutine qui s’exécute en boucle et n”await jamais monopolise la boucle d’événements. Aucune autre tâche ne progresse tant que la boucle ne se termine pas ou ne cède pas la main:
async def counter():
n = 0
while True:
n += 1 # bug: starves the loop
Le correctif est un yield à l’intérieur de la boucle – généralement await asyncio.sleep_ms(0) – afin que les autres tâches prêtes aient une chance de s’exécuter. Les traitements gourmands en calcul relèvent aussi de cette forme : une boucle de traitement d’image qui dure des centaines de millisecondes par itération devrait céder la main au moins une fois par itération pour que le reste du programme ne se bloque pas.
8.15.3. Avaler CancelledError¶
La page sur l’annulation a déjà traité ce point en détail. On le répète ici car c’est la cause la plus fréquente du « mon application refuse de s’arrêter » : une coroutine intercepte asyncio.CancelledError à des fins de nettoyage et oublie de la relever. La tâche continue de s’exécuter ; l’appelant qui a demandé l’annulation reste suspendu indéfiniment à attendre qu’elle se termine. Relevez toujours l’exception après le nettoyage, ou utilisez un bloc try/finally plutôt qu’un except explicite.
8.15.5. await au niveau du module¶
await n’est valide qu’à l’intérieur d’un corps async def. L’écrire au niveau du module – en dehors de toute coroutine – est une erreur de syntaxe:
# bug: not inside an async def
result = await fetch()
Le correctif consiste à placer le travail dans une coroutine et à l’appeler depuis le point d’entrée asyncio.run() du programme.
8.15.6. Plusieurs appels à asyncio.run¶
MicroPython possède une seule boucle d’événements. Appeler asyncio.run() deux fois de suite – une fois pour la configuration, une fois pour le travail principal – utilise quand même la même boucle. L’appeler depuis l’intérieur d’une coroutine en cours d’exécution est une erreur : la boucle tourne déjà. Les deux cas surviennent le plus souvent lorsqu’un script grandit organiquement et que l’auteur tente de l’étendre en ajoutant d’autres appels à run() au lieu d’intégrer le nouveau travail dans le main existant.
8.15.7. Utilisation de Event depuis une interruption¶
asyncio.Event.set() ne peut être appelé en toute sécurité que depuis l’intérieur de la boucle d’événements. L’appeler depuis un gestionnaire d’interruption GPIO présente un risque de corruption. Pour réveiller une tâche depuis une interruption, utilisez plutôt ThreadSafeFlag – la page qui lui est consacrée en décrit la forme.
8.15.8. Appels synchrones longs¶
Une coroutine peut attendre les primitives d’attente propres à asyncio ; tout le reste de ce qu’elle appelle s’exécute de manière synchrone et bloque la boucle jusqu’à son retour. Un time.sleep() bloquant de 200 ms, une écriture sur carte SD qui met 80 ms à se vider, une grosse compression JPEG, un appel à csi.CSI.snapshot() – chacun d’eux retient la boucle d’événements pendant toute sa durée. Le correctif dépend de l’appel :
Pour
time.sleep: remplacez-le parawait asyncio.sleepouawait asyncio.sleep_ms.Pour
csi.CSI.snapshot: utilisez le wrapper de capture asynchrone que construit la page sur la capture.Pour les longs calculs (traitement d’image, encodage JPEG) : acceptez le coût ou découpez le travail en morceaux qui
awaitentre les itérations.
Asyncio ne peut pas rendre un appel synchrone non bloquant. Il peut seulement laisser d’autres coroutines s’exécuter pendant qu’autre chose est en attente.