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ères0. Introduction0.1
.___ Installer
uasyncio sur un appareil vide (matériel)
1. Planification de l'exécution conjointe du programme1.1 .___ Modules
2. bibliothèque uasyncio2.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 planification2.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 classes3.1 .___ Lock
Lock3.1.1 .______
Verrous et délais3.2 .___
Événement3.2.1 .______
Valeur de l' événement
3.3 .___ Barrière
Barrière3.4 .___
Sémaphore3.4.1 .______
Sémaphore limité3.5 .___ File d'
attente File d'
attente3.6 .___ Autres classes de synchronisation
4. Développement de classe pour asyncio4.1 .___ Classes utilisant
wait4.1.1 .______
Utilisation dans les gestionnaires de contexte4.1.2 .______ Attendre
dans la coroutine4.2 .___ Itérateurs asynchrones
4.3 .___ Gestionnaires de contexte asynchrones
5. Exceptions aux délais d'attente et en raison des annulations de tâches5.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âches5.2.2 .______
Coroutines avec timeouts6. Interaction avec les périphériques matériels6.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 UART6.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 astuces7.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
WiFi7.7 .___ Arguments du constructeur de la boucle d'événement
8. Notes pour les débutants8.1 .___ Problème 1: boucles d'événements
8.2 .___ Problème 2: méthodes de verrouillage
8.3
.___ L'approche
uasyncio8.4
.___ Planification dans
uasyncio8.5 .___ Pourquoi une planification collaborative et non basée sur les
threads (
_thread )?
8.6 .___ Interaction
8.7 .___
Interrogation0. IntroductionLa 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é à InternetSur 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 conjointeLa 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 ModulesVoici une liste de modules pouvant s'exécuter sur le périphérique cible.
Bibliothèques1.
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émonstrationLes deux premiers sont les plus utiles car ils donnent des résultats visibles lors de l'accès au
matériel Pyboard .
- 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.
- 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.
- astests.py Programmes de test / démo pour le module aswitch .
- asyn_demos.py Démos simples d'annulation de tâches.
- roundrobin.py Démonstration de la planification circulaire. Également la référence en matière de planification des performances.
- 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.
- chain.py Copié à partir de la documentation Python . Démonstration de la chaîne Coroutine.
- aqtest.py Démonstration de la classe Queue de la bibliothèque uasyncio .
- aremote.py Exemple de pilote de périphérique pour le protocole NEC IR.
- auart.py Démonstration du streaming d'entrée-sortie via Pyboard UART .
- 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.
- iorw.py Démonstration d'un périphérique de lecture / écriture à l'aide d'E / S en streaming.
Programmes de test- asyntest.py Teste les classes de synchronisation dans asyn.py.
- cantest.py Tests d'annulation d'emploi.
Utilitaire1.
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ôleLe répertoire
benchmarks contient des scripts pour vérifier et caractériser le
planificateur uasyncio .
2. bibliothèque uasyncioLe 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énementPrenons l'exemple suivant:
import uasyncio as asyncio async def bar (): count = 0, while True : count + = 1 print ( count ) await asyncio.sleep ( 1 )
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 CoroutinesUne 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 rappelLes 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:
- call_soon - appelez dès que possible. Args: callback callback à exécuter, * args tout argument positionnel peut être suivi d'une virgule.
- call_later - appel après un délai en secondes. Args: délai, rappel, * arguments
- 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 )
2.2.3 RemarquesUne 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 RetardsIl 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. synchronisationIl 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 VerrouLock 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()
3.1.1.Lock et timeoutsAu 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énementL'é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 :
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énementLa 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èreIl 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émaphoreLe 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'attenteLa 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()
et la coroutine de consommation peut fonctionner comme suit:
async def consumer(q): while True: result = await(q.get())
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 synchronisationLa 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 asyncioDans 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 coroutinepeut 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)
À 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 contexteLes 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:
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 enlangage 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
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érateursasynchrones 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)
4.3 Gestionnaires de contexte asynchrones Lesclasses 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()
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âchesCes 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 ExceptionsSi 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')
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 timeoutsComme 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 travailuasyncio 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:
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:
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:
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 .Remarque
Il 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 Timeoutsdé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')
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'équipementLa 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
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 synchronisationLes 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 coroutinesIl 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):
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 UARTprogramme 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):
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 )
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.pyLe 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 HTU21DLe 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 astuces7.1 Le programme se bloque Le gelse 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'étatLors 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 orduresVous 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 TestIl 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)
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 communeSi 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():
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())
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 WiFiLe 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énementsUne 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()
É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()
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
Cette question est discutée ici .8 notes pour les débutantsCes 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énementsUne 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()
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()
Le planificateur dans uasyncio vous permet de créer de telles classes.8.2 Problème 2: méthodes de blocageSupposons 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 UasyncioLa 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
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())
8.4 Planification dans uasyncioPython 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
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)
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 InteractionDans 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 .