Utiliser asyncio pour créer des pilotes de périphériques asynchrones sur MicroPython v.1.12

En étudiant les possibilités de MicroPython à ses fins, je suis tombé sur l'une des implémentations de la bibliothèque asyncio et, après une brève correspondance avec Piter Hinch , l'auteur de la bibliothèque, j'ai réalisé que je devais comprendre plus profondément les principes, les concepts de base et les erreurs typiques d'utilisation des méthodes de programmation asynchrones. De plus, la section pour débutants est juste pour moi.

Ce guide est destiné aux utilisateurs ayant différents niveaux d'expérience avec asyncio , y compris une section spéciale pour les débutants.

Table des matières
0. Introduction
0.1 .___ Installer uasyncio sur un appareil vide (matériel)
1. Planification de l'exécution conjointe du programme
1.1 .___ Modules
2. bibliothèque uasyncio
2.1 .___ Structure du programme: cycle de traitement des événements
2.2 .___ Coroutines
2.2.1 .______ Mise en file d'attente des coroutines pour participer à la planification
2.2.2 .______ Démarrage d'un rappel de fonction ( rappel )
2.2.3 .______ Notes: les coroutines comme méthodes apparentées. Les valeurs de retour.
2.3 .___ Retards
3. La synchronisation et ses classes
3.1 .___ Lock Lock
3.1.1 .______ Verrous et délais
3.2 .___ Événement
3.2.1 .______ Valeur de l' événement
3.3 .___ Barrière Barrière
3.4 .___ Sémaphore
3.4.1 .______ Sémaphore limité
3.5 .___ File d' attente File d' attente
3.6 .___ Autres classes de synchronisation
4. Développement de classe pour asyncio
4.1 .___ Classes utilisant wait
4.1.1 .______ Utilisation dans les gestionnaires de contexte
4.1.2 .______ Attendre dans la coroutine
4.2 .___ Itérateurs asynchrones
4.3 .___ Gestionnaires de contexte asynchrones
5. Exceptions aux délais d'attente et en raison des annulations de tâches
5.1 .___ Exceptions
5.2 .___ Exceptions en raison de délais d'attente et en raison de l'annulation de tâches
5.2.1 .______ Annuler les tâches
5.2.2 .______ Coroutines avec timeouts
6. Interaction avec les périphériques matériels
6.1 .___ Problèmes de synchronisation
6.2 .___ Dispositifs d'interrogation avec coroutines
6.3 .___ Utilisation du moteur de streaming
6.3.1 .______ Exemple de pilote UART
6.4 .___ Développement de pilotes pour un périphérique de streaming
6.5 .___ Exemple complet: pilote aremote.py pour récepteur de télécommande infrarouge.
6.6 .___ Driver pour capteur de température et d'humidité HTU21D.
7. Trucs et astuces
7.1 .___ Le programme se bloque
7.2 .___ uasyncio enregistre l'état
7.3 .___ Collecte des ordures
7.4 .___ Test
7.5 .___ Erreur courante. Cela peut être difficile à trouver.
7.6 .___ Programmation à l'aide de sockets ( sockets )
7.6.1 .______ Problèmes WiFi
7.7 .___ Arguments du constructeur de la boucle d'événement
8. Notes pour les débutants
8.1 .___ Problème 1: boucles d'événements
8.2 .___ Problème 2: méthodes de verrouillage
8.3 .___ L'approche uasyncio
8.4 .___ Planification dans uasyncio
8.5 .___ Pourquoi une planification collaborative et non basée sur les threads ( _thread )?
8.6 .___ Interaction
8.7 .___ Interrogation

0. Introduction

La plupart de ce document suppose une certaine familiarité avec la programmation asynchrone. Pour les débutants, une introduction peut être trouvée dans la section 7.

La bibliothèque uasyncio pour MicroPython comprend un sous-ensemble de la bibliothèque asyncio Python et est destinée à être utilisée sur les microcontrôleurs. En tant que tel, il prend une petite quantité de RAM et est configuré pour changer rapidement de contexte avec une allocation de RAM nulle.

Ce document décrit l'utilisation de uasyncio en mettant l'accent sur la création de pilotes pour les périphériques matériels.

L'objectif est de concevoir les pilotes pour que l'application continue de fonctionner pendant que le pilote attend une réponse de l'appareil. Dans le même temps, l'application reste sensible aux autres événements et à l'interaction de l'utilisateur.

Un autre domaine d'application important d' Asyncio est la programmation réseau: sur Internet, vous pouvez trouver suffisamment d'informations sur ce sujet.

Notez que MicroPython est basé sur Python 3.4 avec les modules complémentaires Python 3.5 minimum. Sauf comme détaillé ci-dessous, les fonctions des versions asyncio antérieures à 3.4 ne sont pas prises en charge. Ce document définit les fonctionnalités prises en charge dans ce sous-ensemble.

Le but de ce guide est d'introduire un style de programmation compatible avec CPython V3.5 et supérieur.

0.1 Installer uasyncio sur un appareil vide (matériel)

Il est recommandé d'utiliser le firmware MicroPython V1.11 ou version ultérieure. Sur de nombreuses plates-formes, l'installation n'est pas requise, car uasyncio® est déjà compilé dans l'assemblage. Pour vérifier, tapez simplement REPL

import uasyncio 

Les instructions suivantes couvrent les cas où les modules ne sont pas préinstallés. Les files d'attente et les modules de synchronisation sont facultatifs, mais sont nécessaires pour exécuter les exemples donnés ici.

Appareil connecté à Internet

Sur un appareil connecté à Internet et exécutant le micrologiciel V1.11 ou version ultérieure, vous pouvez effectuer l'installation à l'aide de la version intégrée upip . Assurez-vous que l'appareil est connecté à votre réseau:

 import upip upip.install ( 'micropython-uasyncio' ) upip.install ( 'micropython-uasyncio.synchro' ) upip.install ( 'micropython-uasyncio.queues' ) 

Les messages d'erreur de upip ne sont pas très utiles. Si vous obtenez une erreur incompréhensible, vérifiez à nouveau la connexion Internet.

Matériel sans connexion Internet ( micropip )

Si le périphérique n'a pas de connexion Internet (par exemple, Pyboard V1.x ), le moyen le plus simple consiste à démarrer l'installation de micropip.py sur l'ordinateur dans le répertoire de votre choix, puis à copier la structure de répertoires résultante sur le périphérique cible. L'utilitaire micropip.py s'exécute sur Python 3.2 ou version ultérieure et s'exécute sur Linux, Windows et OSX. Plus d'informations peuvent être trouvées ici .

Appel typique:

 $ micropip.py install -p ~/rats micropython-uasyncio $ micropip.py install -p ~/rats micropython-uasyncio.synchro $ micropip.py install -p ~/rats micropython-uasyncio.queues 

Un appareil sans connexion Internet (source de copie)

Si vous n'utilisez pas micropip.py , les fichiers doivent être copiés à partir de la source. Les instructions suivantes décrivent comment copier le nombre minimum de fichiers sur le périphérique cible, ainsi que le cas où uasyncio doit être compressé dans un assemblage compilé sous forme de bytecode pour réduire l'espace occupé. Pour la dernière version compatible avec le firmware officiel, les fichiers doivent être copiés depuis le site officiel de micropython-lib .

Clonez la bibliothèque sur l'ordinateur avec la commande

 $ git clone https://github.com/micropython/micropython-lib.git 

Sur le périphérique cible, créez le répertoire uasyncio (facultatif dans le répertoire lib) et copiez-y les fichiers suivants:

• uasyncio / uasyncio / __ init__.py
• uasyncio.core / uasyncio / core.py
• uasyncio.synchro / uasyncio / synchro.py
• uasyncio.queues / uasyncio / queues.py


Ces modules uasyncio peuvent être compressés en bytecode en plaçant le répertoire uasyncio et son contenu dans le port du répertoire modules et en recompilant le contenu.

1. Planification conjointe

La technique d'exécution conjointe de plusieurs tâches est largement utilisée dans les systèmes embarqués, qui offre moins de frais généraux que la planification de thread ( _thread ), évitant de nombreux pièges associés aux threads vraiment asynchrones.

1.1 Modules

Voici une liste de modules pouvant s'exécuter sur le périphérique cible.

Bibliothèques

1. asyn.py fournit les primitives de synchronisation Lock, Event, Barrier, Semaphore, BoundedSemaphore, Condition . Prend en charge l'annulation des tâches via les classes NamedTask et Cancellable .

2. aswitch.py Représente des classes pour l'appariement des commutateurs et des boutons, ainsi qu'un objet programme avec possibilité de retard répété. Les boutons sont une généralisation des commutateurs qui fournissent un état logique plutôt que physique, ainsi que des événements déclenchés par une double et longue pression.

Programmes de démonstration

Les deux premiers sont les plus utiles car ils donnent des résultats visibles lors de l'accès au matériel Pyboard .

  1. aledflash.py Clignote quatre indicateurs Pyboard de manière asynchrone pendant 10 secondes. La démonstration la plus simple de uasyncio . Importez-le pour exécuter.
  2. apoll.py Pilote de périphérique pour l'accéléromètre Pyboard . Montre l'utilisation de coroutines pour interroger un périphérique. Fonctionne pendant 20 s. Importez-le pour exécuter. Nécessite Pyboard V1.x.
  3. astests.py Programmes de test / démo pour le module aswitch .
  4. asyn_demos.py Démos simples d'annulation de tâches.
  5. roundrobin.py Démonstration de la planification circulaire. Également la référence en matière de planification des performances.
  6. attendantable.py Démonstration d'une classe avec une attente. Une façon d'implémenter un pilote de périphérique qui interroge une interface.
  7. chain.py Copié à partir de la documentation Python . Démonstration de la chaîne Coroutine.
  8. aqtest.py Démonstration de la classe Queue de la bibliothèque uasyncio .
  9. aremote.py Exemple de pilote de périphérique pour le protocole NEC IR.
  10. auart.py Démonstration du streaming d'entrée-sortie via Pyboard UART .
  11. auart_hd.py Utilisation de Pyboard UART pour communiquer avec un périphérique à l'aide du protocole semi-duplex. Convient aux appareils, par exemple, en utilisant le jeu de commandes du modem AT.
  12. iorw.py Démonstration d'un périphérique de lecture / écriture à l'aide d'E / S en streaming.

Programmes de test

  1. asyntest.py Teste les classes de synchronisation dans asyn.py.
  2. cantest.py Tests d'annulation d'emploi.

Utilitaire

1. check_async_code.py L'utilitaire est écrit en Python3 pour détecter des erreurs de codage spécifiques qui peuvent être difficiles à trouver. Voir section 7.5.

Contrôle

Le répertoire benchmarks contient des scripts pour vérifier et caractériser le planificateur uasyncio .


2. bibliothèque uasyncio

Le concept asyncio est basé sur l'organisation de la planification de l'exécution conjointe de plusieurs tâches, qui dans ce document sont appelées coroutines .

2.1 Structure du programme: boucle d'événement

Prenons l'exemple suivant:

 import uasyncio as asyncio async def bar (): count = 0, while True : count + = 1 print ( count ) await asyncio.sleep ( 1 ) #  1 loop = asyncio.get_event_loop () loop.create_task ( bar ()) #     loop.run_forever () 

L'exécution du programme continue jusqu'à ce que loop.run_forever soit appelé . À ce stade, l'exécution est contrôlée par le planificateur. La ligne après loop.run_forever ne sera jamais exécutée. Le planificateur exécute le code à barres car il a été mis en file d'attente dans le planificateur loop.create_task . Dans cet exemple trivial, il n'y a qu'une seule barre de coroutine. S'il y en avait d'autres, le planificateur les exécuterait pendant les périodes où la barre était en pause.

La plupart des applications intégrées ont une boucle d'événements continue. Une boucle d'événement peut également être démarrée d'une manière qui permet l'achèvement à l'aide de la méthode de boucle d'événement run_until_complete ; Il est principalement utilisé dans les tests. Des exemples peuvent être trouvés dans le module astests.py .

Une instance de boucle d'événement est un objet unique créé par le premier appel à asyncio.get_event_loop () avec deux arguments entiers facultatifs qui indiquent le nombre de coroutines dans les deux files d'attente - le début et l'attente. En règle générale, les deux arguments auront la même valeur, égale au moins au nombre de coroutines exécutées simultanément dans l'application. Habituellement, la valeur par défaut de 16 est suffisante. Si des valeurs non par défaut sont utilisées, voir Arguments du constructeur de la boucle d'événements (section 7.7.).

Si la coroutine a besoin d'appeler la méthode de boucle d'événements (généralement create_task ), l'appel de asyncio.get_event_loop () (sans arguments) la renverra effectivement.

2.2 Coroutines

Une coroutine est créée comme suit:

 async def foo ( delay_secs ): await asyncio.sleep ( delay_secs ) print ( 'Hello' ) 

Une coroutine peut autoriser le lancement d'autres coroutines à l'aide de l'instruction wait . Une coroutine doit contenir au moins une instruction d' attente . Cela provoque l'exécution de la coroutine avant la fin, avant que l'exécution ne passe à l'instruction suivante. Prenons un exemple:

 await asyncio.sleep ( delay_secs ) await asyncio.sleep ( 0 ) 

La première ligne entraîne une pause du code pendant un certain temps, tandis que d'autres coroutines utilisent ce temps pour leur exécution. Un retard de 0 provoque l'exécution de toutes les coroutines en attente dans un ordre cyclique jusqu'à l'exécution de la ligne suivante. Voir l'exemple de roundrobin.py .

2.2.1. File d'attente pour planifier une coroutine

  • EventLoop.create_task Argument: Coroutine à exécuter. Le planificateur met la coroutine en file d'attente pour démarrer dès que possible. L'appel create_task revient immédiatement. La coroutine dans l'argument est spécifiée en utilisant la syntaxe de l'appel de fonction avec les arguments nécessaires.
  • EventLoop.run_until_complete Argument: Coroutine à exécuter. Le planificateur met la coroutine en file d'attente pour démarrer dès que possible. La coroutine dans l'argument est spécifiée en utilisant la syntaxe de l'appel de fonction avec les arguments nécessaires. L'appel un_until_complete revient lorsque la coroutine est terminée: cette méthode fournit un moyen de quitter le planificateur.
  • en attente Argument: coroutine à exécuter, spécifiée à l'aide de la syntaxe d'appel de fonction. Démarre une coroutine dès que possible. La coroutine en attente est bloquée jusqu'à ce que l'une des coroutines attendues se termine.

Ce qui précède est compatible avec CPython . D' autres méthodes uasyncio sont décrites dans les notes (section 2.2.3.).

2.2.2 Démarrage de la fonction de rappel

Les rappels doivent être des fonctions Python conçues pour s'exécuter dans un court laps de temps. Cela est dû au fait que les coroutines ne pourront pas fonctionner pendant toute la durée de l'exécution d'une telle fonction.

Les méthodes de classe EventLoop suivantes utilisent des rappels:

  1. call_soon - appelez dès que possible. Args: callback callback à exécuter, * args tout argument positionnel peut être suivi d'une virgule.
  2. call_later - appel après un délai en secondes. Args: délai, rappel, * arguments
  3. call_later_ms - appel après un délai en ms. Args: délai, rappel, * arguments .

 loop = asyncio.get_event_loop () loop.call_soon ( foo , 5 ) #    'foo'      5. loop.call_later ( 2 , foo , 5 ) #   2 . loop.call_later_ms ( 50 , foo , 5 ) #   50 . loop.run_forever () 

2.2.3 Remarques

Une coroutine peut contenir une instruction de retour avec des valeurs de retour arbitraires. Pour obtenir cette valeur:

 result = await my_coro () 

Une coroutine peut être limitée par des méthodes et doit contenir au moins une instruction d' attente .

2.3 Retards

Il existe deux options pour organiser les retards dans les coroutines. Pour des retards plus longs et dans les cas où la durée n'a pas besoin d'être précise, vous pouvez utiliser:

 async def foo( delay_secs , delay_ms ): await asyncio.sleep ( delay_secs ) print ( 'Hello' ) await asyncio.sleep_ms ( delay_ms ) 

Pendant de tels retards, le planificateur exécutera d'autres coroutines. Cela peut introduire une incertitude temporelle, car la coroutine appelante ne sera lancée que lorsque celle en cours d'exécution est exécutée. Le délai dépend du développeur de l'application, mais il sera probablement de l'ordre de dizaines ou centaines de ms; ceci est discuté plus loin dans l'interaction avec les périphériques matériels (section 6).

Des retards très précis peuvent être effectués en utilisant les fonctions utime - sleep_ms et sleep_us . Ils conviennent mieux aux courts retards, car le planificateur ne pourra pas exécuter d'autres coroutines pendant que le retard est en cours.

3. synchronisation

Il est souvent nécessaire d'assurer la synchronisation entre les coroutines. Un exemple courant consiste à éviter les «conditions de concurrence» lorsque plusieurs coroutines nécessitent simultanément l'accès à la même ressource. Un exemple est fourni dans astests.py et est discuté dans la documentation . Un autre danger est «l'étreinte de la mort», lorsque chaque coroutine attend l'achèvement de l'autre.

Dans les applications simples, la synchronisation peut être réalisée à l'aide d'indicateurs globaux ou de variables associées. Une approche plus élégante consiste à utiliser des classes de synchronisation. Le module asyn.py propose des implémentations «micro» des classes Event, Barrier, Semaphore et Conditios , destinées à être utilisées uniquement avec asyncio . Ils ne sont pas orientés thread et ne doivent pas être utilisés avec le module _thread ou le gestionnaire d'interruption, sauf indication contraire. La classe Lock est également implémentée, ce qui est une alternative à l'implémentation officielle.

Un autre problème de synchronisation se pose avec les producteurs et les consommateurs de coroutine. Un producteur de coroutine génère des données qu'un consommateur de coroutine utilise. Pour ce faire, asyncio fournit la classe Queue . Le producteur de coroutine place les données dans la file d'attente, tandis que le consommateur de coroutine attend sa fin (avec d'autres opérations planifiées à temps). La classe Queue fournit des garanties pour supprimer les éléments dans l'ordre dans lequel ils ont été reçus. Vous pouvez également utiliser la classe Barrière si la coroutine de producteur doit attendre que la coroutine de consommateur soit prête à accéder aux données.

Un bref aperçu des classes est donné ci-dessous. Plus de détails dans la documentation complète .

3.1 Verrou

Lock garantit un accès unique à une ressource partagée. L'exemple de code suivant crée une instance de la classe de verrouillage Lock qui est transmise à tous les clients qui souhaitent accéder à la ressource partagée. Chaque coroutine essaie de capturer le verrou, suspendant l'exécution jusqu'à ce qu'il réussisse:

 import uasyncio as asyncio from uasyncio.synchro import Lock async def task(i, lock): while 1: await lock.acquire() print("Acquired lock in task", i) await asyncio.sleep(0.5) lock.release() async def killer(): await asyncio.sleep(10) loop = asyncio.get_event_loop() lock = Lock() # The global Lock instance loop.create_task(task(1, lock)) loop.create_task(task(2, lock)) loop.create_task(task(3, lock)) loop.run_until_complete(killer()) #  10s 

3.1.1.Lock et timeouts

Au moment de la rédaction (5 janvier 2018), le développement de la classe uasycio Lock n'était pas officiellement terminé. Si la coroutine a un délai d'attente (section 5.2.2.) , Lors de l'attente d'un verrouillage lorsqu'elle est déclenchée, le délai d'attente sera inefficace. Il ne recevra pas une TimeoutError jusqu'à ce qu'il reçoive un verrou. Il en va de même pour l'annulation d'une tâche.

Le module asyn.py propose la classe Lock , qui fonctionne dans ces situations. Cette implémentation de la classe est moins efficace que la classe officielle, mais prend en charge des interfaces supplémentaires selon la version de CPython , y compris l'utilisation du gestionnaire de contexte.

3.2 Événement

L'événement crée une occasion pour une ou plusieurs coroutines de faire une pause, tandis qu'un autre donne un signal sur leur poursuite. Une instance d' Event devient disponible pour toutes les coroutines qui l'utilisent:

 import asyn event = asyn.Event () 

Une coroutine attend un événement en déclarant un événement wait , après quoi l'exécution s'interrompt jusqu'à ce que d'autres coroutines déclarent event.set () . Information complète .

Un problème peut survenir si event.set () est émis dans une construction en boucle; le code doit attendre que tous les objets en attente aient accès à l'événement avant de le redéfinir. Dans le cas où un coro attend un événement, cela peut être réalisé en recevant un événement coro effaçant l'événement:

 async def eventwait ( event ): await event event.clear() 

La coroutine qui déclenche l'événement vérifie qu'il a été traité:

 async def foo ( event ): while True : #   - while event.is_set (): await asyncio.sleep ( 1 ) # ,  coro   event.set () 

Dans le cas où plusieurs coros attendent la synchronisation d'un événement, le problème peut être résolu en utilisant l'événement de confirmation. Chaque coro a besoin d'un événement distinct.

 async def eventwait (  , ack_event ): await event ack_event.set () 

Un exemple de ceci est donné dans la fonction event_test dans asyntest.py . C'est lourd Dans la plupart des cas - même avec un coro en attente - la classe Barrière , présentée ci-dessous, offre une approche plus simple.
Un événement peut également fournir un moyen de communication entre le gestionnaire d'interruption et le coro . Le gestionnaire gère le matériel et définit l'événement, qui est vérifié par coro déjà en mode normal.

3.2.1 Valeurs d'événement

La méthode event.set () peut prendre une valeur de données facultative de n'importe quel type. Coro , en attente d'un événement, peut l'obtenir avec event.value () . Notez que event.clear () sera défini sur Aucun . Une utilisation typique de ceci pour le réglage de coro de l'événement est d'émettre event.set (utime.ticks_ms ()) . Tout coro en attente d'un événement peut déterminer le retard qui s'est produit, par exemple, pour compenser cela.

3.3 Barrière

Il existe deux utilisations pour la classe Barrière .

Tout d'abord, il peut suspendre une coroutine jusqu'à ce qu'une ou plusieurs autres coroutines soient terminées.

Deuxièmement, il permet à plusieurs coroutines de se rencontrer à un certain point. Par exemple, un producteur et un consommateur peuvent se synchroniser au point où le producteur a des données et le consommateur est prêt à les utiliser. Au moment de l'exécution, la barrière peut émettre un rappel supplémentaire avant la suppression de la barrière et tous les événements en attente peuvent se poursuivre.

Le rappel peut être une fonction ou une coroutine. Dans la plupart des applications, la fonction sera très probablement utilisée: elle peut être garantie d'être exécutée avant la fin, avant que la barrière ne soit supprimée.

Un exemple est la fonction barrière_test dans asyntest.py . Dans l'extrait de code de ce programme:

 import asyn def callback(text): print(text) barrier = asyn.Barrier(3, callback, ('Synch',)) async def report(): for i in range(5): print('{} '.format(i), end='') await barrier 

plusieurs instances de la coroutine de rapport impriment leur résultat et s'arrêtent jusqu'à ce que d'autres instances soient également terminées et attendent que la barrière continue. À ce stade, un rappel est en cours. À la fin, la coroutine d'origine reprend.

3.4 Sémaphore

Le sémaphore limite le nombre de coroutines pouvant accéder à la ressource. Il peut être utilisé pour limiter le nombre d'instances d'une coroutine particulière pouvant s'exécuter simultanément. Cela se fait à l'aide d'un compteur d'accès, qui est initialisé par le constructeur et réduit à chaque fois que la coroutine reçoit un sémaphore.

La façon la plus simple de l'utiliser dans un gestionnaire de contexte:

 import asyn sema = asyn.Semaphore(3) async def foo(sema): async with sema: #    

Un exemple est la fonction semaphore_test dans asyntest.py .

3.4.1 Sémaphore ( limité )

Elle fonctionne de manière similaire à la classe Semaphore , sauf que si la méthode de libération fait dépasser le compteur d'accès de sa valeur initiale, une ValueError est définie.

3.5 File d'attente

La classe Queue est maintenue par le uasycio officiel et l'exemple de programme aqtest.py montre son utilisation. La file d'attente est créée comme suit:

 from uasyncio.queues import Queue q = Queue () 

Une coroutine de fabricant typique peut fonctionner comme suit:

 async def producer(q): while True: result = await slow_process() #       await q.put(result) #  ,        

et la coroutine de consommation peut fonctionner comme suit:

 async def consumer(q): while True: result = await(q.get()) # ,  q  print('Result was {}'.format(result)) 

La classe Queue fournit des fonctionnalités supplémentaires importantes lorsque la taille des files d'attente peut être limitée et que l'état peut être interrogé. Le comportement avec une file d'attente vide (si la taille est limitée) et le comportement avec une file d'attente pleine peuvent être contrôlés.La documentation à ce sujet se trouve dans le code.

3.6 Autres classes de synchronisation

La bibliothèque asyn.py fournit une micro implémentation de certaines des autres fonctionnalités de CPython .

La classe Condition permet à une coroutine de notifier les autres coroutines en attente sur une ressource verrouillée. Après avoir reçu la notification, ils auront accès à la ressource et se déverrouilleront à leur tour. Une coroutine de notification peut limiter le nombre de coroutines à notifier.

La classe Gather vous permet d'exécuter une liste de coroutines. A l'issue de ce dernier, une liste de résultats vous sera retournée. Cette implémentation "micro" utilise une syntaxe différente. Des délais d'attente peuvent être appliqués à n'importe laquelle des coroutines.

4 Développement de classes pour asyncio

Dans le cadre du développement de pilotes de périphériques, l'objectif est de s'assurer qu'ils fonctionnent sans blocage. Un pilote de coroutine doit s'assurer que les autres coroutines sont exécutées pendant que le pilote attend que le périphérique effectue des opérations matérielles. Par exemple, une tâche en attente de données arrivant dans UART ou un utilisateur appuyant sur un bouton doit permettre la planification d'autres événements jusqu'à ce que l'événement se produise.

4.1 Classes utilisant l' attente d' attente Une coroutine

peut suspendre l'exécution en attendant un objet attendable . Sous CPython, une classe attendue personnalisée est créée en implémentant la méthode spéciale __await__que le générateur retourne. La classe attendue est utilisée comme suit:

 import uasyncio as asyncio class Foo(): def __await__(self): for n in range(5): print('__await__ called') yield from asyncio.sleep(1) #     return 42 __iter__ = __await__ # .   async def bar(): foo = Foo() # Foo - awaitable  print('waiting for foo') res = await foo #   print('done', res) loop = asyncio.get_event_loop() loop.run_until_complete(bar()) 

À l' heure actuelle MicroPython pas en charge __await__ ( numéro 2678 ) et pour que la solution soit utilisée __iter__ . La chaîne __iter__ = __await__ fournit la portabilité entre CPython et MicroPython . Les exemples de code, voir les classes d' événements, barrière, révocable, Condition dans asyn.py .

4.1.1 Utilisation dans les gestionnaires de contexte

Les objets attendus peuvent être utilisés dans les gestionnaires de contexte synchrones ou asynchrones, en fournissant les méthodes spéciales nécessaires. Syntaxe:

 with await awaitable as a: #  'as'   #    async with awaitable as a: #    (.) #  - 

Pour ce faire, le générateur __await__ doit retourner lui- même . Ceci est passé à n'importe quelle variable dans la clause as et permet également aux méthodes spéciales de fonctionner. Voir asyn.Condition et asyntest.condition_test où la classe Condition utilise attend et peut être utilisée dans un gestionnaire de contexte synchrone.

4.1.2 coroutine Attendent en

langage Python nécessite __await__ est la fonction génératrice. Dans MicroPython, les générateurs et les coroutines sont identiques, donc la solution est d'utiliser le rendement de coro (args) .

Le but de ce guide est de proposer du code portable vers CPython 3.5 ou version ultérieure. Dans CPython, les générateurs et les coroutines ont une signification différente. En CPython, une coroutine a une méthode spéciale __await__ que le générateur récupère. C'est portable:

 up = False #   MicroPython? try: import uasyncio as asyncio up = True #    sys.implementation.name except ImportError: import asyncio async def times_two(n): # Coro   await asyncio.sleep(1) return 2 * n class Foo(): def __await__(self): res = 1 for n in range(5): print('__await__ called') if up: # MicroPython res = yield from times_two(res) else: # CPython res = yield from times_two(res).__await__() return res __iter__ = __await__ async def bar(): foo = Foo() # foo is awaitable print('waiting for foo') res = await foo #   print('done', res) loop = asyncio.get_event_loop() loop.run_until_complete(bar()) 

Notez que __await__, le rendement de asyncio.sleep (1) est autorisé par CPython . Je ne comprends toujours pas comment cela est réalisé.

4.2 Itérateurs

asynchrones Les itérateurs asynchrones fournissent un moyen de renvoyer une séquence finie ou infinie de valeurs et peuvent être utilisés comme moyen de récupérer des éléments de données séquentiels lorsqu'ils proviennent d'un périphérique en lecture seule. Un itérateur asynchrone appelle du code asynchrone dans sa méthode suivante . La classe doit répondre aux exigences suivantes:

  • Il a une méthode __aiter__ définie dans async def et renvoyant un itérateur asynchrone.
  • Il a une méthode __anext__ , qui est elle-même une coroutine - c'est-à-dire définie via async def et contenant au moins une instruction wait . Pour arrêter l'itération, elle doit déclencher une exception StopAsyncIteration .

Les valeurs en série sont récupérées en utilisant async pour comme indiqué ci-dessous:

 class AsyncIterable: def __init__(self): self.data = (1, 2, 3, 4, 5) self.index = 0 async def __aiter__(self): return self async def __anext__(self): data = await self.fetch_data() if data: return data else: raise StopAsyncIteration async def fetch_data(self): await asyncio.sleep(0.1) #     if self.index >= len(self.data): return None x = self.data[self.index] self.index += 1 return x async def run(): ai = AsyncIterable() async for x in ai: print(x) 

4.3 Gestionnaires de contexte asynchrones Les

classes peuvent être conçues pour prendre en charge les gestionnaires de contexte asynchrones qui ont des procédures d'entrée et de sortie qui sont des co-programmes. Un exemple est la classe de verrouillage décrite ci-dessus. Il possède la coroutine __aenter__ , qui est logiquement requise pour un fonctionnement asynchrone. Pour prendre en charge le protocole asynchrone du gestionnaire de contexte, sa méthode __aexit__ doit également être une coroutine, ce qui est obtenu en incluant attend asyncio.sleep (0) . Ces classes sont accessibles depuis une coroutine avec la syntaxe suivante:

 async def bar ( lock ): async with lock: print ( « bar » ) 

Comme c'est le cas avec les gestionnaires de contexte classiques, la méthode de sortie est garantie d'être appelée lorsque le gestionnaire de contexte termine son travail, comme d'habitude, et par le biais d'une exception. Pour atteindre cet objectif, des méthodes spéciales __aenter__ et __aexit__ sont utilisées et doivent être définies comme des coroutines attendant une autre coroutine ou un objet attendu . Cet exemple est tiré de la classe Lock :

  async def __aenter__(self): await self.acquire() # a coro    async def return self async def __aexit__(self, *args): self.release() #   await asyncio.sleep_ms(0) 

Si async avec contient une clause as variable , la variable obtient la valeur renvoyée par __aenter__ .

Pour garantir un comportement correct, le micrologiciel doit être V1.9.10 ou version ultérieure.

5. Exceptions aux délais d'expiration et en raison de l'annulation de tâches

Ces sujets sont liés: uasyncio inclut l'annulation de tâches et l'application d'un délai d'expiration à une tâche, lançant une exception pour la tâche d'une manière spéciale.

5.1 Exceptions

Si une exception se produit dans une coroutine, elle doit être traitée soit dans cette coroutine, soit dans une coroutine en attente de son achèvement. Cela garantit que l'exception ne s'applique pas au planificateur. Si une exception se produit, le planificateur cesse de fonctionner en transmettant l'exception au code que le planificateur a démarré. Par conséquent, pour éviter l'arrêt du planificateur, la coroutine lancée avec loop.create_task () doit intercepter toutes les exceptions à l'intérieur.

Utiliser throw ou close pour lever une exception dans une coroutine est déraisonnable. Cela détruit uasyncio , provoquant le démarrage et éventuellement la sortie de la coroutine lorsqu'elle se trouve encore dans la file d'attente d'exécution.

L'exemple ci-dessus illustre cette situation. S'il est autorisé à fonctionner jusqu'à la fin, il fonctionne comme prévu.

 import uasyncio as asyncio async def foo(): await asyncio.sleep(3) print('About to throw exception.') 1/0 async def bar(): try: await foo() except ZeroDivisionError: print('foo  -   0') # ! raise #     . except KeyboardInterrupt: print('foo was interrupted by ctrl-c') #   ! raise async def shutdown(): print('Shutdown is running.') #     await asyncio.sleep(1) print('done') loop = asyncio.get_event_loop() try: loop.run_until_complete(bar()) except ZeroDivisionError: loop.run_until_complete(shutdown()) except KeyboardInterrupt: print('Keyboard interrupt at loop level.') loop.run_until_complete(shutdown()) 

Cependant, l'émission d'une interruption de clavier entraîne l'exception dans la boucle d'événements. En effet, l'exécution de uasyncio.sleep est passée à la boucle d'événements. Par conséquent, les applications qui nécessitent un code clair en réponse à une interruption du clavier doivent intercepter une exception au niveau de la boucle d'événements.

5.2 Annulation et timeouts

Comme mentionné ci-dessus, ces fonctions fonctionnent en lançant une exception pour la tâche d'une manière spéciale en utilisant la méthode spéciale MicroPython coroutine pend_throw . Le fonctionnement dépend de la version. Dans uasyncio v.2.0 officiel , une exception n'est pas traitée jusqu'à la prochaine tâche planifiée. Cela impose un délai si la tâche attend le sommeilentrée-sortie Les délais d'attente peuvent dépasser leur période nominale. La tâche d'annulation d'autres tâches ne peut pas déterminer quand l'annulation est terminée.

Il existe actuellement une solution de contournement et deux solutions.

  • Solution : la bibliothèque asyn offre un moyen d'attendre l'annulation des tâches ou des groupes de tâches. Voir Annuler un travail (section 5.2.1.).
  • La bibliothèque Paul Sokolovsky fournit uasyncio v2.4 , mais cela nécessite son firmware Pycopy .
  • Fast_io bibliothèque uasyncio permetrésoudre ce problème dans le Python (de manière moins élégante) etcoursexécution firmware officiel.

La hiérarchie des exceptions utilisée ici est Exception-CanceledError-TimeoutError .

5.2.1 Annulation d'un travail

uasyncio propose une fonction d' annulation (coro) . Cela fonctionne en lançant une exception pour utiliser la coroutine pend_throw . Il fonctionne également avec les coroutines imbriquées. L'utilisation est la suivante:

 async def foo(): while True: #  -  10 secs await asyncio.sleep(10) async def bar(loop): foo_instance = foo() #   coro loop.create_task(foo_instance) # code omitted asyncio.cancel(foo_instance) 

Si cet exemple est exécuté sous uasyncio v2.0 , lorsque les problèmes de barre s'annulent, il ne prendra effet qu'au prochain foo programmé , et il peut y avoir un retard pouvant aller jusqu'à 10 secondes lorsque foo est annulé . Une autre source de retard se produira si foo attend les E / S. Partout où le retard se produit, le bar ne pourra pas déterminer si le foo a été annulé. Cela compte dans certains cas d'utilisation.

Lors de l'utilisation des bibliothèques Paul Sokolovsky ou fast_io, il suffit d'utiliser sleep (0):

 async def foo(): while True: #  -  10 secs await asyncio.sleep(10) async def bar(loop): foo_instance = foo() #   coro loop.create_task(foo_instance) #    asyncio.cancel(foo_instance) await asyncio.sleep(0) #    

Cela fonctionnera également dans uasyncio v2.0 si foo (et tout foo coroutine en attente ) n'est jamais revenu en veille et n'a pas attendu les E / S.

Un comportement qui peut surprendre les imprudents se produit lorsqu'une coroutine exécutée par create_task et en mode veille est censée s'annuler . Considérez cet extrait:

 async def foo(): while True: #  -  10 secs await asyncio.sleep(10) async def foo_runner(foo_instance): await foo_instance print('   ') async def bar(loop): foo_instance = foo() loop.create_task(foo_runner(foo_instance)) #    asyncio.cancel(foo_instance) 

Lorsque foo est annulé, il est supprimé de la file d'attente du planificateur; car il manque une instruction de retour , la procédure d'appel foo_runner ne reprend jamais. Il est recommandé de toujours intercepter l'exception dans la portée la plus externe de la fonction à annuler:

 async def foo(): try: while True: await asyncio.sleep(10) await my_coro except asyncio.CancelledError: return 

Dans ce cas, my_coro n'a pas besoin d'attraper l'exception, car il sera propagé sur le canal appelant et capturé à cet endroit .

RemarqueIl est interdit d'utiliser les méthodes close ou throw des coroutines lorsque les coroutines sont utilisées en dehors du planificateur. Cela sape le planificateur, forçant la coroutine à exécuter du code, même s'il n'est pas planifié. Cela peut avoir des conséquences indésirables.

5.2.2 Coroutines avec Timeouts

délais d' attente mis en oeuvre en utilisant uasyncio méthodes .wait_for () et .wait_for_ms () . Ils prennent la coroutine et la latence en secondes ou ms, respectivement, comme arguments. Si le délai expire, une TimeoutError sera lancée dans la coroutine à l'aide de pend_throw. Cette exception doit être interceptée par l'utilisateur ou par l'appelant. Cela est nécessaire pour la raison décrite ci-dessus: si le délai expire, il est annulé. À moins que l'erreur ne soit interceptée et renvoyée, la seule façon pour l'appelant de continuer est d'attraper l'exception elle-même.

Lorsque l'exception a été interceptée par la coroutine, j'ai eu des échecs peu clairs si l'exception n'a pas été interceptée dans la portée externe, comme indiqué ci-dessous:

 import uasyncio as asyncio async def forever(): try: print('Starting') while True: await asyncio.sleep_ms(300) print('Got here') except asyncio.TimeoutError: print('Got timeout') # And return async def foo(): await asyncio.wait_for(forever(), 5) await asyncio.sleep(2) loop = asyncio.get_event_loop() loop.run_until_complete(foo()) 

Alternativement, vous pouvez intercepter la fonction appelante:

 import uasyncio as asyncio async def forever(): print('Starting') while True: await asyncio.sleep_ms(300) print('Got here') async def foo(): try: await asyncio.wait_for(forever(), 5) except asyncio.TimeoutError: pass print('Timeout elapsed.') await asyncio.sleep(2) loop = asyncio.get_event_loop() loop.run_until_complete(foo()) 

Remarque pour Uasyncio v2.0 .

Cela ne s'applique pas aux bibliothèques Paul Sokolovsky ou fast_io .

Si la coroutine démarre, attendez asyncio.sleep (t) , avec un long retard t, la coroutine ne redémarrera pas avant l'expiration de t . Si le délai d'attente s'est écoulé avant la fin du sommeil , une erreur TimeoutError se produit lorsque la coroutine est rechargée - c'est-à-dire quand t expire . En temps réel et du point de vue de l'appelant, sa réponse TimeoutError sera retardée.

Si cela est important pour l'application, créez un long délai en attendant un court dans la boucle. Coroutineasyn.sleep prend cela en charge.

6 Interaction avec l'équipement

La base de l'interaction entre uasyncio et les événements asynchrones externes est l'interrogation. Le matériel nécessitant une réponse rapide peut utiliser une interruption. Mais l'interaction entre la routine d'interruption (ISR) et la coroutine utilisateur sera basée sur des sondages. Par exemple, un ISR peut appeler Event ou définir un indicateur global, tandis qu'une coroutine en attente d'un résultat interroge un objet chaque fois qu'une demande est planifiée.

L'interrogation peut être réalisée de deux manières, explicite ou implicite. Ce dernier se fait à l'aide d' E / S de fluxun mécanisme, qui est un système conçu pour les appareils de streaming tels que UART et les sockets . Dans le sondage explicite le plus simple, le code suivant peut consister:

 async def poll_my_device(): global my_flag #   ISR while True: if my_flag: my_flag = False # service the device await asyncio.sleep(0) 

Au lieu d'un indicateur global, vous pouvez utiliser une variable d'instance de la classe Event ou une instance d'une classe qui utilise wait . Une enquête explicite est discutée ci-dessous.

L'interrogation implicite consiste à développer un pilote qui agira comme un périphérique d'E / S de streaming, tel qu'un socket UART ou Stream I / O , qui interroge les périphériques à l'aide du système Python select.poll : puisque l'interrogation est effectuée en C, elle est plus rapide et plus efficace que sondage explicite. L'utilisation d'E / S de flux est discutée dans la section 6.3.

En raison de son efficacité, l'interrogation implicite donne un avantage aux pilotes de périphériques d'E / S les plus rapides: des pilotes de streaming peuvent être créés pour de nombreux périphériques qui ne sont généralement pas considérés comme des périphériques de streaming. Ceci est discuté plus en détail dans la section 6.4.

6.1 Problèmes de synchronisation

Les sondages explicites et implicites reposent actuellement sur une planification cyclique. Supposons que les E / S fonctionnent simultanément avec N coroutines personnalisées, chacune fonctionnant avec un retard nul. Lorsque les E / S sont servies, elles seront interrogées dès que toutes les opérations utilisateur sont planifiées. Le délai estimé doit être pris en compte lors de la conception. Les canaux d'E / S peuvent nécessiter une mise en mémoire tampon, avec un équipement de maintenance ISR en temps réel à partir des tampons et des coroutines, remplissant ou libérant les tampons plus lentement.

Il faut aussi considérer la possibilité d'aller au-delà: c'est le cas lorsque quelque chose interrogé par la coroutine se produit plus d'une fois avant qu'il ne soit effectivement planifié par la coroutine.

Un autre problème de synchronisation est la précision de la latence. Si la coroutine émet

 await asyncio.sleep_ms ( t ) #   

le planificateur garantit que l'exécution sera suspendue pendant au moins t ms. Le retard réel peut être supérieur à t, ce qui dépend de la charge actuelle du système. Si à ce moment d'autres coroutines attendent la fin des délais non nuls, la prochaine ligne sera immédiatement programmée pour exécution. Mais si d'autres coroutines sont également en attente d'exécution (soit parce qu'elles ont émis un délai nul, soit parce que leur délai a également expiré), elles peuvent être programmées pour s'exécuter plus tôt. Cela introduit une incertitude de synchronisation dans les fonctions sleep () et sleep_ms () . La pire valeur pour ce débordement peut être calculée en additionnant les valeurs d'exécution de toutes ces coroutines pour déterminer le temps de transmission le plus défavorable au planificateur.

La version fast_io de uasyncio dans ce contexte fournit un moyen de garantir que les E / S de streaming seront interrogées à chaque itération du planificateur. Nous espérons que l' uasyncio officiel acceptera les amendements pertinents en temps voulu .

6.2 Interroger des appareils à l'aide de coroutines

Il s'agit d'une approche simple qui convient le mieux aux appareils pouvant être interrogés à une vitesse relativement faible. Cela est principalement dû au fait que l'interrogation avec un intervalle d'interrogation court (ou nul) peut conduire au fait que la coroutine consomme plus de temps processeur que ce qui est souhaitable pour tomber dans l'intervalle.

L'exemple apoll.py illustre cette approche en interrogeant l'accéléromètre Pyboardavec un intervalle de 100 ms. Il effectue un filtrage simple pour ignorer le bruit et imprime un message toutes les deux secondes si aucun mouvement ne se produit.

L'exemple aswitch.py fournit des pilotes pour les commutateurs et les périphériques bouton.

Un exemple de pilote pour un périphérique capable de lire et d'écrire est illustré ci-dessous. Pour faciliter les tests, Pyboard UART 4 ​​émule un périphérique conditionnel. Le pilote implémente la classe RecordOrientedUart, dans lequel les données sont fournies dans des enregistrements de longueur variable composés d'instances d'octets. L'objet ajoute un délimiteur avant d'envoyer et met en mémoire tampon les données entrantes jusqu'à ce qu'un délimiteur ajouté soit reçu. C'est juste une démo et une manière inefficace d'utiliser UART par rapport à l'entrée / sortie en streaming.

Afin de démontrer le transfert asynchrone, nous supposons que le périphérique émulé a un moyen de vérifier que le transfert est terminé et que l'application nous oblige à attendre. Aucune des hypothèses n'est vraie dans cet exemple, mais le code la simule en attendant asyncio.sleep (0.1) .

Pour commencer, n'oubliez pas de connecter les sorties du Pyboard X1 et X2 (UART Txd et Rxd)

 import uasyncio as asyncio from pyb import UART class RecordOrientedUart(): DELIMITER = b'\0' def __init__(self): self.uart = UART(4, 9600) self.data = b'' def __iter__(self): # Not __await__ issue #2678 data = b'' while not data.endswith(self.DELIMITER): yield from asyncio.sleep(0) # ,  : while not self.uart.any(): yield from asyncio.sleep(0) # timing may mean this is never called data = b''.join((data, self.uart.read(self.uart.any()))) self.data = data async def send_record(self, data): data = b''.join((data, self.DELIMITER)) self.uart.write(data) await self._send_complete() #          #        await asyncio.sleep(0) async def _send_complete(self): await asyncio.sleep(0.1) def read_record(self): # Synchronous: await the object before calling return self.data[0:-1] # Discard delimiter async def run(): foo = RecordOrientedUart() rx_data = b'' await foo.send_record(b'A line of text.') for _ in range(20): await foo #  coros       foo rx_data = foo.read_record() print('Got: {}'.format(rx_data)) await foo.send_record(rx_data) rx_data = b'' loop = asyncio.get_event_loop() loop.run_until_complete(run()) 

6.3 Utilisation du mécanisme de streaming ( Stream )

L'exemple illustre l'entrée-sortie simultanée sur un UART du microprocesseur Pyboard .

Pour commencer, connectez les sorties du Pyboard X1 et X2 (UART Txd et Rxd)

 import uasyncio as asyncio from pyb import UART uart = UART(4, 9600) async def sender(): swriter = asyncio.StreamWriter(uart, {}) while True: await swriter.awrite('Hello uart\n') await asyncio.sleep(2) async def receiver(): sreader = asyncio.StreamReader(uart) while True: res = await sreader.readline() print('Received', res) loop = asyncio.get_event_loop() loop.create_task(sender()) loop.create_task(receiver()) loop.run_forever() 

Le code pris en charge se trouve dans __init__.py dans la bibliothèque uasyncio . Le mécanisme fonctionne car le pilote de périphérique (écrit en C ) implémente les méthodes suivantes: ioctl, read, readline et write . Section 6.4: Écrire un pilote de périphérique de streaming révèle des détails sur la façon dont ces pilotes peuvent être écrits en Python .

L'UART peut recevoir des données à tout moment. Le mécanisme d'E / S de streaming vérifie les caractères entrants en attente chaque fois que le planificateur prend le contrôle. Lorsque la coroutine est en cours d'exécution, la routine d'interruption met en mémoire tampon les caractères entrants; ils seront supprimés lorsque la coroutine fera place au planificateur. Par conséquent, les applications UART doivent être conçues de manière à ce que les coroutines minimisent le temps entre les transferts vers le planificateur afin d'éviter les dépassements de tampon et la perte de données. Cela peut être amélioré en utilisant un tampon de lecture UART plus grand ou un débit de données inférieur. Alternativement, le contrôle de flux matériel fournira une solution si la source de données le prend en charge.

6.3.1 Exemple de pilote UART

programme auart_hd.pyillustre une méthode de communication avec un périphérique semi-duplex, tel qu'un périphérique répondant à l'ensemble de commandes du modem AT. Le semi-duplex signifie que l'appareil n'envoie jamais de données non sollicitées: ses transferts sont toujours effectués en réponse à une commande reçue du maître.

L'appareil est émulé en exécutant un test sur un Pyboard avec deux connexions câblées.

Le périphérique émulé (très simplifié) répond à n'importe quelle commande en envoyant quatre lignes de données avec une pause entre chacune pour simuler un traitement lent.

L'assistant envoie une commande, mais ne sait pas à l'avance combien de lignes de données seront renvoyées. Il démarre un temporisateur de redémarrage qui redémarre chaque fois qu'une ligne est reçue. Lorsque le temporisateur expire, il est supposé que le périphérique a terminé la transmission et une liste des lignes reçues est renvoyée.

Un cas de défaillance de périphérique est également démontré, ce qui est obtenu en sautant une transmission avant d'attendre une réponse. Après le délai d'attente, une liste vide est renvoyée. Voir les commentaires de code pour plus de détails.

6.4 Développement de pilotes en continu ( flux ) unité

d'entrée de courant / mécanisme de sortie ( flux E / S ) pour commander le fonctionnement des dispositifs d' E / S en continu tels que les UART et les douilles ( socket) Le mécanisme peut être utilisé par les pilotes de tout périphérique régulièrement interrogé en déléguant au planificateur qui utilise select , interrogeant l'état de préparation de tous les périphériques de la file d'attente. C'est plus efficace que d'effectuer plusieurs opérations de coroutine, dont chacune interroge le périphérique, en partie parce que select est écrit en C , et aussi parce que la coroutine qui effectue l'interrogation est retardée jusqu'à ce que l'objet interrogé retourne un état prêt.

Un pilote de périphérique capable de prendre en charge le mécanisme d'entrée / sortie de streaming doit de préférence prendre en charge les méthodes StreamReader, StreamWriter. Un périphérique lisible doit fournir au moins l'une des méthodes suivantes. Veuillez noter qu'il s'agit de méthodes synchrones. La méthode ioctl (voir ci-dessous) garantit qu'ils ne sont appelés que lorsque les données sont disponibles. Les méthodes doivent être renvoyées le plus rapidement possible, en utilisant autant de données que possible.

readline () Retourne autant de caractères que possible, jusqu'à n'importe quel caractère de nouvelle ligne. Obligatoire si vous utilisez StreamReader.readline ()

read (n) Renvoyez autant de caractères que possible, mais pas plus de n . Obligatoire si vous utilisez StreamReader.read () ou StreamReader.readexactly ()

Le pilote créé doit fournir la méthode synchrone suivante avec retour immédiat:

écrire avec les arguments buf, off, sz .

Où:

buf est le tampon d'écriture.
off - décalé vers le tampon du premier caractère à écrire.
sz - le nombre de caractères requis pour écrire.
La valeur de retour est le nombre de caractères réellement écrits (peut-être 1 si l'appareil est lent).
La méthode ioctl garantit qu'elle ne sera appelée que lorsque l'appareil est prêt à recevoir des données.

Tous les appareils doivent fournir une méthode ioctl qui interroge l'équipement pour déterminer son état de disponibilité. Un exemple typique pour un pilote de lecture / écriture:

 import io MP_STREAM_POLL_RD = const(1) MP_STREAM_POLL_WR = const(4) MP_STREAM_POLL = const(3) MP_STREAM_ERROR = const(-1) class MyIO(io.IOBase): #    def ioctl(self, req, arg): # see ports/stm32/uart.c ret = MP_STREAM_ERROR if req == MP_STREAM_POLL: ret = 0 if arg & MP_STREAM_POLL_RD: if hardware_has_at_least_one_char_to_read: ret |= MP_STREAM_POLL_RD if arg & MP_STREAM_POLL_WR: if hardware_can_accept_at_least_one_write_character: ret |= MP_STREAM_POLL_WR return ret 

Voici une description du délai d'attente de la classe MillisecTimer :

 import uasyncio as asyncio import utime import io MP_STREAM_POLL_RD = const(1) MP_STREAM_POLL = const(3) MP_STREAM_ERROR = const(-1) class MillisecTimer(io.IOBase): def __init__(self): self.end = 0 self.sreader = asyncio.StreamReader(self) def __iter__(self): await self.sreader.readline() def __call__(self, ms): self.end = utime.ticks_add(utime.ticks_ms(), ms) return self def readline(self): return b'\n' def ioctl(self, req, arg): ret = MP_STREAM_ERROR if req == MP_STREAM_POLL: ret = 0 if arg & MP_STREAM_POLL_RD: if utime.ticks_diff(utime.ticks_ms(), self.end) >= 0: ret |= MP_STREAM_POLL_RD return ret 

qui peut être utilisé comme suit:

 async def timer_test ( n ): timer = ms_timer.MillisecTimer () await timer ( 30 ) #  30  

Par rapport à uasyncio officiel , une telle implémentation n'offre aucun avantage par rapport à l' attente asyncio.sleep_ms () . L'utilisation de fast_io fournit des retards significativement plus précis dans le modèle d'utilisation normal, lorsque les coroutines s'attendent à un retard nul.

Vous pouvez utiliser la planification des E / S pour associer un événement à un rappel. C'est plus efficace que le cycle d'interrogation, car l'interrogation n'est pas planifiée tant que ioctl n'est pas prêt. Ensuite, un rappel est exécuté lorsque le rappel change d'état.

 import uasyncio as asyncio import io MP_STREAM_POLL_RD = const(1) MP_STREAM_POLL = const(3) MP_STREAM_ERROR = const(-1) class PinCall(io.IOBase): def __init__(self, pin, *, cb_rise=None, cbr_args=(), cb_fall=None, cbf_args=()): self.pin = pin self.cb_rise = cb_rise self.cbr_args = cbr_args self.cb_fall = cb_fall self.cbf_args = cbf_args self.pinval = pin.value() self.sreader = asyncio.StreamReader(self) loop = asyncio.get_event_loop() loop.create_task(self.run()) async def run(self): while True: await self.sreader.read(1) def read(self, _): v = self.pinval if v and self.cb_rise is not None: self.cb_rise(*self.cbr_args) return b'\n' if not v and self.cb_fall is not None: self.cb_fall(*self.cbf_args) return b'\n' def ioctl(self, req, arg): ret = MP_STREAM_ERROR if req == MP_STREAM_POLL: ret = 0 if arg & MP_STREAM_POLL_RD: v = self.pin.value() if v != self.pinval: self.pinval = v ret = MP_STREAM_POLL_RD return ret 

Et encore une fois - sur l' uasyncio officiel , le retard peut être élevé. Selon la conception de l'application, la version fast_io peut être plus efficace.

La démo iorw.py illustre un exemple complet. Veuillez noter qu'au moment de la rédaction de l'article dans l' uasyncio officiel, il y a une erreur qui ne fonctionne pas . Il y a deux solutions. La solution consiste à écrire deux pilotes distincts, l'un en lecture seule et l'autre en écriture seule. La seconde consiste à utiliser fast_io , qui résout ce problème.

Dans uasyncio officiel , les entrées / sorties sont prévues assez rarement .

6.5 Exemple complet: aremote.py

Le pilote est conçu pour recevoir / décoder les signaux d'une télécommande infrarouge. Le pilote aremote.py lui-même . Les notes suivantes sont importantes concernant l'utilisation de asyncio .

L'interruption sur le contact enregistre l'heure du changement d'état (en microsecondes) et définit l'événement, sautant l'heure à laquelle le premier changement d'état s'est produit. La coroutine attend un événement, signale la durée du paquet de données, puis décode les données stockées avant d'appeler le rappel spécifié par l'utilisateur.

Le passage du temps à une instance d' événement permet à la coroutine de compenser toutretard asynchrone lors de la définition de la période de retard.

6.6 Capteur environnemental HTU21D

Le pilote de puce HTU21D fournit des mesures précises de température et d'humidité.

La puce a besoin d'environ 120 ms pour recevoir les deux éléments de données. Le pilote fonctionne de manière asynchrone, initiant la réception et l'utilisation de l' attente asyncio.sleep (t) avant de lire les données, met à jour les variables de température et d'humidité, accessibles à tout moment, ce qui permet de lancer d'autres coroutines pendant que le pilote de la puce est en cours d'exécution.

7. Trucs et astuces

7.1 Le programme se bloque Le gel

se produit généralement parce que la tâche est bloquée sans concession: cela entraînera le gel de l'ensemble du système. Lors du développement, il est utile d'avoir une coroutine qui allume périodiquement la LED intégrée. Cela confirme que le planificateur est toujours en cours d'exécution.

7.2 uasyncio enregistre l'état

Lors du démarrage de programmes utilisant uasyncio dans REPL, effectuez une réinitialisation logicielle (ctrl-D) entre les démarrages. Du fait que uasyncio conserve son état entre les démarrages, un comportement imprévisible peut se produire au prochain démarrage.

7.3 Collecte des ordures

Vous pouvez exécuter une coroutine en spécifiant d'abord gc d'importation :

 gc.collect () gc.treshold ( gc.mem_free () // 4 + gc.mem_alloc ()) 

Le but de ceci est discuté ici dans la section tas.

7.4 Test

Il est conseillé de s'assurer que le pilote de périphérique garde le contrôle si nécessaire, ce qui peut être fait en exécutant une ou plusieurs copies de coroutines fictives qui démarrent le cycle d'impression des messages et en vérifiant qu'il s'exécute pendant les périodes où le pilote est en mode veille:

 async def rr(n): while True: print('Roundrobin ', n) await asyncio.sleep(0) 

À titre d'exemple du type de danger pouvant survenir, dans l'exemple ci-dessus, la méthode RecordOrientedUart __await__ a été initialement écrite comme suit :

 def __await__(self): data = b'' while not data.endswith(self.DELIMITER): while not self.uart.any(): yield from asyncio.sleep(0) data = b''.join((data, self.uart.read(self.uart.any()))) self.data = data 

En conséquence, l'exécution est étirée jusqu'à ce que l'enregistrement entier soit reçu, ainsi que le fait que uart.any () renvoie toujours un nombre différent de zéro de caractères reçus. Au moment de l'appel, tous les caractères peuvent déjà être reçus. Cette situation peut être résolue à l'aide d'une boucle externe:

 def __await__(self): data = b'' while not data.endswith(self.DELIMITER): yield from asyncio.sleep(0) # ,  : while not self.uart.any(): yield from asyncio.sleep(0) #        data = b''.join((data, self.uart.read(self.uart.any()))) self.data = data 

Il convient de noter que cette erreur n'aurait pas été évidente si les données avaient été envoyées à l'UART à une vitesse inférieure, plutôt que d'utiliser un test de rétroaction. Bienvenue aux joies de la programmation en temps réel.

7.5 Erreur commune

Si une fonction ou une méthode est définie par async def et ensuite appelée comme s'il s'agissait d'un appel régulier (synchrone), MicroPython n'affiche pas de message d'erreur. C'est par conception. Cela conduit généralement au fait que le programme ne fonctionne pas correctement en silence:

 async def foo(): # code loop.create_task(foo) #  1 1: foo     foo() #  2: . 

J'ai une suggestion qui suggère de corriger la situation dans l'option 1 en utilisant fast_io .

Le module check_async_code.py essaie de détecter les cas d'utilisation douteuse de coroutines. Il est écrit en Python3 et est conçu pour fonctionner sur un PC. Utilisé dans des scripts écrits conformément aux directives décrites dans ce guide avec des coroutines déclarées utilisant async def . Le module prend un argument, le chemin d'accès au fichier MicroPython source (ou --help).

Veuillez noter qu'il est quelque peu grossier et est destiné à être utilisé dans un fichier syntaxiquement correct, qui ne démarre pas par défaut. Utilisez un outil, tel que pylint, pour la vérification générale de la syntaxe ( pylint n'a actuellement pas cette erreur).

Le script produit des faux positifs. Selon le plan, les coroutines sont des objets de premier niveau; elles peuvent être transférées vers des fonctions et stockées dans des structures de données. Selon la logique du programme, vous pouvez enregistrer la fonction ou le résultat de son exécution. Le script ne peut pas déterminer l'intention. Il vise à ignorer les cas qui semblent corrects lors de l'identification d'autres cas à considérer. Supposons foo où la coroutine est déclarée async def :

 loop.run_until_complete(foo()) #   bar(foo) #     ,      bar(foo()) z = (foo,) z = (foo(),) foo() #  :   . 

Je le trouve utile tel quel, mais les améliorations sont toujours les bienvenues.

7.6 Programmation avec prises ( prises )

Il existe deux approches de base pour la programmation des prises uasyncio . Par défaut, les sockets sont bloqués jusqu'à la fin de l'opération de lecture ou d'écriture spécifiée. Uasyncio prend en charge le verrouillage de socket à l'aide de select.poll pour empêcher le planificateur de les bloquer. Dans la plupart des cas, ce mécanisme est plus simple à utiliser. Un exemple de code client et serveur se trouve dans le répertoire client_server . Userver utilise l'application select.poll en interrogeant explicitement le socket du serveur.

Les sockets client l'utilisent implicitement dans le sens où le moteur de streaming uasyncio l' utilise directement.

Veuillez noter que socket.getaddrinfo est actuellement bloqué. Le temps dans l'exemple de code sera minimal, mais si une recherche DNS est requise, la période de blocage peut être importante.

Une deuxième approche de la programmation des sockets consiste à utiliser des sockets non bloquants. Cela ajoute de la complexité, mais est nécessaire dans certaines applications, surtout si la connexion se fait via WiFi (voir ci-dessous).

Au moment d'écrire ces lignes (mars 2019), le support TLS pour les sockets non bloquants était en cours de développement. Son statut exact est inconnu (pour moi).

L'utilisation de prises non bloquantes nécessite une certaine attention aux détails. Si des lectures non bloquantes se produisent en raison de la latence du serveur, il n'y a aucune garantie que toutes (ou certaines) des données demandées seront retournées. De même, les entrées peuvent ne pas se terminer.

Par conséquent, les méthodes de lecture et d'écriture asynchrones doivent effectuer de manière itérative une opération non bloquante jusqu'à ce que les données requises soient lues ou écrites. En pratique, un délai d'attente peut être nécessaire pour faire face aux pannes de serveur.
Une autre complication est que le port ESP32 avait des problèmes qui nécessitaient des effractions assez désagréables pour un fonctionnement sans erreur. Je n'ai pas vérifié si c'est toujours le cas.
Module Sock_nonblock.pyillustre les méthodes requises. Ce n'est pas une démonstration de travail et les décisions sont susceptibles de dépendre de l'application.

7.6.1 Problèmes avec le WiFi

Le mécanisme de streaming uasyncio n'est pas la meilleure option pour détecter les pannes WiFi. J'ai trouvé nécessaire d'utiliser des sockets non bloquants pour fournir un fonctionnement à sécurité intégrée et reconnecter le client en cas de panne.

Ce document décrit les problèmes que j'ai rencontrés dans les applications WiFi qui gardent les sockets ouverts pendant de longues périodes et décrit la solution.

Pltcmoffre un client MQTT asynchrone robuste qui assure l'intégrité des messages lors des pannes WiFi. Une simple liaison série en duplex intégral asynchrone entre un client sans fil et un serveur câblé avec livraison garantie des messages est décrite.

7.7 Arguments du constructeur de la boucle d'événements

Une petite erreur peut se produire si vous devez créer une boucle d'événements avec des valeurs qui diffèrent des valeurs par défaut. Une telle boucle doit être déclarée avant d'exécuter tout autre code utilisant asyncio car ces valeurs peuvent être requises dans ce code. Sinon, le code sera initialisé avec des valeurs par défaut:

 import uasyncio as asyncio import some_module bar = some_module.Bar() #   get_event_loop() #     loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) 

Étant donné que l'importation d'un module peut exécuter du code, le moyen le plus sûr consiste à instancier une boucle d'événements immédiatement après l'importation d' uasyncio .

 import uasyncio as asyncio loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) import some_module bar = some_module.Bar() # get_event_loop()    

Lors de l'écriture de modules à utiliser par d'autres programmes, je préfère éviter d'exécuter du code uasyncio lors de l'importation. Ecrivez des fonctions et des méthodes pour attendre une boucle d'événement comme argument. Assurez-vous ensuite que seules les applications de niveau supérieur appellent get_event_loop :

 import uasyncio as asyncio import my_module #      loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) bar = my_module.Bar(loop) 

Cette question est discutée ici .

8 notes pour les débutants

Ces notes sont destinées aux débutants en code asynchrone et commencent par une description des problèmes que les planificateurs tentent de résoudre, ainsi que donner un aperçu de l' approche uasyncio des solutions.

La section 8.5 présente les avantages relatifs des modules uasyncio et _ thread , ainsi que les raisons pour lesquelles vous préférerez peut-être utiliser les coroutines uasyncio avec une planification proactive (_thread).

8.1 Problème 1: boucles d'événements

Une application de firmware typique fonctionne en continu et doit en même temps répondre à des événements externes, qui peuvent inclure un changement de tension sur l'ADC, l'apparition d'une interruption matérielle, ou un symbole reçu dans l'UART, ou des données disponibles sur la prise. Ces événements se produisent de manière asynchrone et le code doit pouvoir répondre quel que soit l'ordre dans lequel ils se produisent. De plus, des tâches en fonction du temps peuvent être nécessaires, telles que des voyants clignotants.

La façon évidente de le faire est d' utiliser la boucle d' événements uasycio . Cet exemple n'est pas un code pratique, mais sert à illustrer la forme générale de la boucle d'événements.

 def event_loop(): led_1_time = 0 led_1_period = 20 led_2_time = 0 led_2_period = 30 switch_state = switch.state() #    while True: time_now = utime.time() if time_now >= led_1_time: #  LED #1 led1.toggle() led_1_time = time_now + led_1_period if time_now >= led_2_time: #  LED #2 led2.toggle() led_2_time = time_now + led_2_period #    LEDs if switch.value() != switch_state: switch_state = switch.value() #  - if uart.any(): #    UART 

Une telle boucle fonctionne pour des exemples simples, mais à mesure que le nombre d'événements augmente, le code devient rapidement lourd. Ils violent également les principes de la programmation orientée objet en combinant la plupart de la logique du programme en un seul endroit, plutôt que de lier le code à un objet contrôlé. Nous voulons développer une classe pour une LED clignotante qui peut être insérée dans un module et importée. L'approche OOP du clignotement des LED peut ressembler à ceci:

 import pyb class LED_flashable(): def __init__(self, led_no): self.led = pyb.LED(led_no) def flash(self, period): while True: self.led.toggle() # -     period, #          

Le planificateur dans uasyncio vous permet de créer de telles classes.

8.2 Problème 2: méthodes de blocage

Supposons que vous ayez besoin de lire un certain nombre d'octets à partir d'un socket. Si vous appelez socket.read (n) avec une socket bloquante par défaut, il "bloquera" (c'est-à-dire qu'il ne pourra pas se terminer) jusqu'à ce que n octets soient reçus . Pendant cette période, l'application ne répondra pas aux autres événements.

À l'aide du socket uasyncio non bloquant , vous pouvez écrire une méthode de lecture asynchrone. Une tâche qui nécessite des données sera (nécessairement) bloquée jusqu'à leur réception, mais d'autres tâches seront effectuées pendant cette période, ce qui permettra à l'application de rester réactive.

8.3. Approches Uasyncio

La classe suivante a une LED qui peut être allumée et éteinte, et vous pouvez également clignoter à n'importe quelle vitesse. L'instance LED_async utilise la méthode run , qui peut être utilisée pour un fonctionnement continu. Le comportement des LED peut être contrôlé à l'aide des méthodes on (), off () et flash (secs) .

 import pyb import uasyncio as asyncio class LED_async(): def __init__(self, led_no): self.led = pyb.LED(led_no) self.rate = 0 loop = asyncio.get_event_loop() loop.create_task(self.run()) async def run(self): while True: if self.rate <= 0: await asyncio.sleep_ms(200) else: self.led.toggle() await asyncio.sleep_ms(int(500 / self.rate)) def flash(self, rate): self.rate = rate def on(self): self.led.on() self.rate = 0 def off(self): self.led.off() self.rate = 0 

Il convient de noter que on (), off () et flash () sont des méthodes synchrones ordinaires. Ils modifient le comportement de la LED, mais reviennent immédiatement. Le clignotement se produit "en arrière-plan". Ceci est expliqué en détail dans la section suivante.

La classe respecte le principe OOP, qui consiste à stocker dans la classe la logique associée au dispositif. Dans le même temps, l'utilisation de uasyncio garantit que l'application peut répondre à d'autres événements pendant que le voyant clignote. Le programme ci-dessous clignote avec quatre LED Pyboard à différentes fréquences, et répond également au bouton USR, qui le complète.

 import pyb import uasyncio as asyncio from led_async import LED_async # ,   async def killer(): # ,      sw = pyb.Switch() while not sw.value(): await asyncio.sleep_ms(100) leds = [LED_async(n) for n in range(1, 4)] for n, led in enumerate(leds): led.flash(0.7 + n/4) loop = asyncio.get_event_loop() loop.run_until_complete(killer()) 

Contrairement au premier exemple de boucle d'événement, la logique associée au commutateur est dans une fonction distincte de la fonctionnalité de la LED. Faites attention au code utilisé pour démarrer le planificateur:

 loop = asyncio.get_event_loop() loop.run_until_complete(killer()) #    #       killer (), #   . 

8.4 Planification dans uasyncio

Python 3.5 et MicroPython prennent en charge le concept d'une fonction asynchrone, également appelée coroutine ou tâche. Une coroutine doit inclure au moins une instruction d' attente .

 async def hello(): for _ in range(10): print('Hello world.') await asyncio.sleep(1) 

Cette fonction imprime un message dix fois à des intervalles d'une seconde. Pendant que la fonction est suspendue en prévision d'un retard, le planificateur asyncio effectuera d'autres tâches, créant l'illusion de les exécuter simultanément.

Lorsqu'un problème de coroutine attend asyncio.sleep_ms () ou attend asyncio.sleep (), la tâche en cours est suspendue et placée dans une file d'attente qui est ordonnée par le temps et l'exécution passe à la tâche en haut de la file d'attente. La file d'attente est conçue de telle sorte que, même si le mode veille spécifié est zéro, d'autres tâches pertinentes seront exécutées jusqu'à la reprise du courant. Il s'agit d'une planification «honnête circulaire». Il est courant d'exécuter des boucles asyncio.sleep (0) en attente .afin que la tâche ne retarde pas l'exécution. Ce qui suit est une boucle d'occupation en attente d'une autre tâche pour définir la variable d' indicateur global . Hélas, il monopolise le processeur, empêchant le lancement d'autres coroutines:

 async def bad_code(): global flag while not flag: pass #  flag = False #     

Le problème ici est que jusqu'à ce que la boucle flagis False passe le contrôle au planificateur, aucune autre tâche ne sera démarrée. La bonne approche:

 async def good_code(): global flag while not flag: await asyncio.sleep(0) #  flag = False #     

Pour la même raison, il est incorrect de définir des retards, par exemple, utime.sleep (1) car il bloque les autres tâches pendant 1 s; il est plus correct d'utiliser attendre asyncio.sleep (1) .
Veuillez noter que les retards générés par les méthodes uasyncio sleep et sleep_ms peuvent en fait dépasser la durée spécifiée. Cela est dû au fait que d'autres tâches seront effectuées pendant le retard. Après la période de retard avant l' exécution reprend jusqu'à ce que la tâche en cours d' exécution ne donnera pas await ou quittes. Une coroutine bien élevée déclarera toujours attendreà intervalles réguliers. Lorsqu'un délai exact est requis, en particulier s'il est inférieur à quelques ms, il peut être nécessaire d'utiliser utime.sleep_us (us) .

8.5 Pourquoi une planification collaborative et non basée sur les threads ( _thread )?

La réaction initiale des débutants à l'idée de co-planification des coroutines est souvent décevante. La planification du streaming est sûrement mieux? Pourquoi devrais-je explicitement donner le contrôle de la manière si la machine virtuelle Python peut le faire pour moi?

En matière de systèmes embarqués, le modèle de collaboration présente deux avantages.
Le premier est léger. Il est possible d'avoir un grand nombre de coroutines, car contrairement aux threads planifiés, les coroutines suspendues prennent moins de place.
Deuxièmement, cela évite certains des problèmes subtils associés à la planification du streaming.

En pratique, le multitâche collaboratif est largement utilisé, en particulier dans les applications d'interface utilisateur.

Pour défendre le modèle de planification du streaming, je vais montrer un avantage: si quelqu'un écrit

 for x in range ( 1000000 ): #  -  

il ne bloquera pas d'autres tâches. Le modèle de collaboration suppose que la boucle doit explicitement donner au contrôle de chaque tâche un certain nombre d'itérations, par exemple, mettre du code dans une coroutine et émettre périodiquement en attendant asyncio.sleep (0) .

Hélas, cet avantage est pâle par rapport aux inconvénients. Certains d'entre eux sont décrits dans la documentation pour l'écriture des gestionnaires d'interruption.. Dans un modèle de planification de streaming, chaque thread peut interrompre tout autre thread, modifiant les données qui peuvent être utilisées dans d'autres threads. En règle générale, il est beaucoup plus facile de trouver et de corriger un verrou qui se produit en raison d'une erreur qui ne donne pas de résultat que la détection d'erreurs parfois très subtiles et rarement rencontrées qui peuvent se produire dans du code écrit dans le cadre d'un modèle avec planification en streaming.

Autrement dit, si vous écrivez une coroutine MicroPython , vous pouvez être sûr que les variables ne seront pas soudainement modifiées par une autre coroutine: votre coroutine a le contrôle total jusqu'à ce qu'elle retourne attend asyncio.sleep (0) .

Gardez à l'esprit que les gestionnaires d'interruption sont préventifs. Cela s'applique aux interruptions matérielles et logicielles qui peuvent se produire n'importe où dans votre code.

Une discussion éloquente sur les problèmes de planification du streaming peut être trouvée ici .

8.6 Interaction

Dans les applications non triviales, les coroutines doivent interagir. Des méthodes Python conventionnelles peuvent être utilisées . Il s'agit notamment d'utiliser des variables globales ou de déclarer des coroutines comme méthodes d'objet: elles peuvent partager des variables d'instance. Alternativement, un objet mutable peut être passé comme argument à une coroutine.

Le modèle de planification de la diffusion en continu nécessite des spécialistes pour garantir que les classes fournissent une connexion sécurisée; dans un modèle de collaboration, cela est rarement nécessaire.

8.7. Sondage ( Polling )

Certains périphérique matériel tel qu'un accéléromètre Pyboard , ne prend pas en charge les interruptions, et doit donc être interrogé ( à savoir vérifié périodiquement). L'interrogation peut également être utilisée conjointement avec des gestionnaires d'interruption: le gestionnaire d'interruption maintient l'équipement et définit un indicateur. La coroutine interroge le drapeau - s'il est défini, les données sont traitées et le drapeau est réinitialisé. La meilleure approche consiste à utiliser la classe Event .

Source: https://habr.com/ru/post/fr484472/


All Articles