Verwenden von asyncio zum Erstellen asynchroner Gerätetreiber unter MicroPython v.1.12

Als ich die Möglichkeiten von MicroPython für seine Zwecke studierte, stieß ich auf eine der Implementierungen der Asyncio- Bibliothek und nach einer kurzen Korrespondenz mit Piter Hinch , dem Autor der Bibliothek, erkannte ich, dass ich die Prinzipien, Grundkonzepte und typischen Fehler bei der Verwendung asynchroner Programmiermethoden besser verstehen musste. Außerdem ist der Abschnitt für Anfänger nur für mich.

Dieses Handbuch richtet sich an Benutzer mit unterschiedlichen Erfahrungen mit Asyncio , einschließlich eines speziellen Abschnitts für Anfänger.

Inhalt
0. Einleitung
0.1 .___ uasyncio auf einem leeren Gerät (Hardware) installieren
1. Planung für die gemeinsame Programmausführung
1.1 .___ Module
2. Uasyncio- Bibliothek
2.1 .___ Programmstruktur: Ereignisverarbeitungszyklus
2.2 .___ Koroutinen
2.2.1 .______ Einreihen von Coroutinen zur Teilnahme an der Planung
2.2.2 .______ Funktionsrückruf starten ( Callback )
2.2.3 .______ Anmerkungen: Koroutinen als verwandte Methoden. Die Rückgabewerte.
2.3 .___ Verspätungen
3. Synchronisation und ihre Klassen
3.1 .___ Sperre Sperre
3.1.1 .______ Sperren und Zeitüberschreitungen
3.2 .___ Ereignis
3.2.1 .______ Ereigniswert
3.3 .___ Barriere Barriere
3.4 .___ Semaphor
3.4.1 .______ Limited Semaphore
3.5 .___ Warteschlange Warteschlange
3.6 .___ Andere Synchronisationsklassen
4. Klassenentwicklung für Asyncio
4.1 .___ Klassen mit wait
4.1.1 .______ Verwendung in Kontextmanagern
4.1.2 .______ In der Coroutine warten
4.2 .___ Asynchrone Iteratoren
4.3 .___ Asynchrone Kontextmanager
5. Ausnahmen zu Zeitüberschreitungen und aufgrund von Aufgabenstornierungen
5.1 .___ Ausnahmen
5.2 .___ Ausnahmen aufgrund von Timeouts und aufgrund von Abbrüchen von Aufgaben
5.2.1 .______ Aufgaben abbrechen
5.2.2 .______ Koroutinen mit Timeouts
6. Interaktion mit Hardwaregeräten
6.1 .___ Synchronisierungsprobleme
6.2 .___ Abfragen von Geräten mit Coroutinen
6.3 .___ Verwenden der Streaming-Engine
6.3.1 .______ UART- Treiberbeispiel
6.4 .___ Treiberentwicklung für ein Streaming-Gerät
6.5 .___ Vollständiges Beispiel: aremote.py Treiber für IR-Fernbedienungsempfänger.
6.6 .___ Treiber für Temperatur- und Feuchtigkeitssensor HTU21D.
7. Tipps und Tricks
7.1 .___ Programm friert ein
7.2 .___ uasyncio speichert den Zustand
7.3 .___ Speicherbereinigung
7.4 .___ Testen
7.5 .___ Häufiger Fehler. Es kann schwer zu finden sein.
7.6 .___ Programmieren mit Sockets ( Sockets )
7.6.1 .______ WLAN-Probleme
7.7 .___ Argumente des Event-Loop-Konstruktors
8. Hinweise für Anfänger
8.1 .___ Problem 1: Ereignisschleifen
8.2 .___ Problem 2: Sperrmethoden
8.3 .___ Der Uasyncio- Ansatz
8.4 .___ Planung in uasyncio
8.5 .___ Warum kollaboratives, nicht threadbasiertes Scheduling ( _thread )?
8.6 .___ Interaktion
8.7 .___ Polling

0. Einleitung

Der größte Teil dieses Dokuments setzt eine gewisse Vertrautheit mit der asynchronen Programmierung voraus. Für Anfänger finden Sie eine Einführung in Abschnitt 7.

Die uasyncio- Bibliothek für MicroPython enthält eine Teilmenge der asyncio- Python- Bibliothek und ist für die Verwendung auf Mikrocontrollern vorgesehen. Daher nimmt es nur wenig RAM ein und ist so konfiguriert, dass Kontexte ohne RAM-Zuweisung schnell umgeschaltet werden.

In diesem Dokument wird die Verwendung von uasyncio beschrieben, wobei der Schwerpunkt auf der Erstellung von Treibern für Hardwaregeräte liegt.

Ziel ist es, die Treiber so zu gestalten, dass die Anwendung weiterhin funktioniert, während der Treiber auf eine Antwort vom Gerät wartet. Gleichzeitig reagiert die Anwendung empfindlich auf andere Ereignisse und Benutzerinteraktionen.

Ein weiteres wichtiges Einsatzgebiet von asyncio ist die Netzwerkprogrammierung: Im Internet finden Sie genügend Informationen zu diesem Thema.

Beachten Sie, dass MicroPython auf Python 3.4 mit den minimalen Python 3.5- Add-Ons basiert. Funktionen von Asyncio- Versionen, die älter als 3.4 sind, werden nicht unterstützt, es sei denn, dies wird im Folgenden beschrieben . In diesem Dokument werden die Funktionen definiert, die in dieser Untergruppe unterstützt werden.

In diesem Handbuch wird ein Programmierstil vorgestellt, der mit CPython V3.5 und höher kompatibel ist.

0.1 Installieren Sie uasyncio auf einem leeren Gerät (Hardware)

Es wird empfohlen, die Firmware MicroPython V1.11 oder höher zu verwenden. Auf vielen Plattformen ist keine Installation erforderlich, da uasyncio® bereits in der Assembly kompiliert ist. Um dies zu überprüfen, geben Sie einfach REPL ein

import uasyncio 

Die folgenden Anweisungen decken Fälle ab, in denen die Module nicht vorinstalliert sind. Die Warteschlangen und Synchronisationsmodule sind optional, werden jedoch benötigt, um die hier angegebenen Beispiele auszuführen.

Gerät mit Internetverbindung

Auf einem mit dem Internet verbundenen Gerät, auf dem Firmware V1.11 oder höher ausgeführt wird, können Sie mit der integrierten Upip- Version installieren. Stellen Sie sicher, dass das Gerät mit Ihrem Netzwerk verbunden ist:

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

Die Fehlermeldungen von upip sind nicht sehr nützlich. Wenn Sie einen unverständlichen Fehler erhalten, überprüfen Sie die Internetverbindung erneut.

Hardware ohne Internetverbindung ( Micropip )

Wenn Ihr Gerät nicht über eine Internetverbindung verfügt (z. B. Pyboard V1.x ), starten Sie am einfachsten die Installation von micropip.py auf dem Computer in das Verzeichnis Ihrer Wahl und kopieren Sie die resultierende Verzeichnisstruktur auf das Zielgerät. Das Dienstprogramm micropip.py läuft unter Python 3.2 oder höher und unter Linux, Windows und OSX. Weitere Informationen finden Sie hier .

Typischer Anruf:

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

Ein Gerät ohne Internetverbindung (Kopierquelle)

Wenn Sie micropip.py nicht verwenden, müssen die Dateien von der Quelle kopiert werden. In den folgenden Anweisungen wird beschrieben, wie die Mindestanzahl von Dateien auf das Zielgerät kopiert wird. Außerdem wird der Fall beschrieben, in dem uasyncio in Form eines Bytecodes in eine kompilierte Assembly komprimiert werden muss, um den belegten Speicherplatz zu verringern. Für die neueste Version, die mit der offiziellen Firmware kompatibel ist, müssen die Dateien von der offiziellen Micropython-Lib- Website kopiert werden.

Klonen Sie die Bibliothek mit dem Befehl auf den Computer

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

Erstellen Sie auf dem Zielgerät das Verzeichnis uasyncio (optional im Verzeichnis lib) und kopieren Sie die folgenden Dateien hinein:

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


Diese uasyncio- Module können zu Bytecode komprimiert werden, indem das Verzeichnis uasyncio und sein Inhalt in den Port des Verzeichnisses modules gestellt und der Inhalt neu kompiliert wird.

1. Gemeinsame Planung

Die Technik der gemeinsamen Ausführung mehrerer Tasks ist in eingebetteten Systemen weit verbreitet und bietet weniger Overhead als die Thread- Planung ( _thread ), wodurch viele Fallstricke vermieden werden, die mit wirklich asynchronen Threads verbunden sind.

1.1 Module

Im Folgenden finden Sie eine Liste der Module, die auf dem Zielgerät ausgeführt werden können.

Bibliotheken

1. asyn.py Stellt Sperre, Ereignis, Barriere, Semaphor, BoundedSemaphor, Bedingung und Sammelsynchronisationsprimitive bereit . Bietet Unterstützung für das Abbrechen von Aufgaben über die Klassen NamedTask und Cancellable .

2. aswitch.py Repräsentiert Klassen zum Koppeln von Schaltern und Tasten sowie ein Programmobjekt mit der Möglichkeit einer wiederholten Verzögerung. Tasten sind eine Verallgemeinerung von Schaltern, die eher einen logischen als einen physischen Zustand bereitstellen, sowie von Ereignissen, die durch zweimaliges und langes Drücken ausgelöst werden.

Demo-Programme

Die ersten beiden sind am nützlichsten, da sie beim Zugriff auf die Pyboard-Hardware sichtbare Ergebnisse liefern .

  1. aledflash.py Blinkt vier Pyboard- Anzeigen 10 Sekunden lang asynchron. Die einfachste Demonstration von Uasyncio . Importieren Sie es zum Ausführen.
  2. apoll.py Gerätetreiber für den Pyboard- Beschleunigungssensor. Veranschaulicht die Verwendung von Coroutinen zum Abfragen eines Geräts. Funktioniert für 20 s. Importieren Sie es zum Ausführen. Benötigt Pyboard V1.x.
  3. astests.py Test- / Demoprogramme für das Aswitch- Modul.
  4. asyn_demos.py Einfache Demos zum Abbrechen von Aufgaben.
  5. roundrobin.py Demonstration der Kreisplanung . Auch der Maßstab für die Leistungsplanung.
  6. awaitable.py Demonstration einer Klasse mit einer Wartezeit. Eine Möglichkeit, einen Gerätetreiber zu implementieren, der eine Schnittstelle abfragt.
  7. chain.py Kopiert aus der Python- Dokumentation. Demonstration der Coroutine-Kette.
  8. aqtest.py Demonstration der Queue- Klasse der uasyncio- Bibliothek.
  9. aremote.py Beispielgerätetreiber für das NEC-IR-Protokoll.
  10. auart.py Demonstration von Streaming Input-Output über Pyboard UART .
  11. auart_hd.py Verwenden von Pyboard UART zur Kommunikation mit einem Gerät unter Verwendung des Halbduplex-Protokolls. Geeignet für Geräte, die beispielsweise den Befehlssatz AT-Modem verwenden.
  12. iorw.py Demonstration eines Lese- / Schreibgeräts mit Streaming-E / A.

Testprogramme

  1. asyntest.py Prüft auf Synchronisationsklassen in asyn.py.
  2. cantest.py Abbruchtests .

Dienstprogramm

1. check_async_code.py Das Dienstprogramm wurde in Python3 geschrieben , um bestimmte Codierungsfehler zu erkennen, die möglicherweise schwer zu finden sind. Siehe Abschnitt 7.5.

Kontrolle

Das Benchmark- Verzeichnis enthält Skripte zur Überprüfung und Charakterisierung des uasyncio-Schedulers .


2. Uasyncio- Bibliothek

Das asyncio- Konzept basiert auf der Organisation der Planung für die gemeinsame Ausführung mehrerer Aufgaben, die in diesem Dokument als Coroutinen bezeichnet werden .

2.1 Programmstruktur: Ereignisschleife

Betrachten Sie das folgende Beispiel:

 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 () 

Die Programmausführung wird fortgesetzt, bis loop.run_forever aufgerufen wird . Zu diesem Zeitpunkt wird die Ausführung vom Scheduler gesteuert. Die Zeile nach loop.run_forever wird niemals ausgeführt. Der Scheduler führt den Barcode aus , da er im Scheduler loop.create_task in die Warteschlange gestellt wurde . In diesem einfachen Beispiel gibt es nur einen Koroutinenstab. Wenn es andere gäbe, würde der Scheduler sie in Zeiträumen ausführen, in denen die Leiste angehalten wurde.

Die meisten eingebetteten Anwendungen verfügen über eine kontinuierliche Ereignisschleife. Eine Ereignisschleife kann auch so gestartet werden, dass sie mit der Ereignisschleifenmethode run_until_complete abgeschlossen werden kann. Es wird hauptsächlich zum Testen verwendet. Beispiele finden Sie im astests.py- Modul.

Eine Ereignisschleifeninstanz ist ein einzelnes Objekt, das durch den ersten Aufruf von asyncio.get_event_loop () mit zwei optionalen Ganzzahlargumenten erstellt wird, die die Anzahl der Coroutinen in den beiden Warteschlangen angeben - Start und Warten. In der Regel haben beide Argumente den gleichen Wert, der mindestens der Anzahl der gleichzeitig ausgeführten Coroutinen in der Anwendung entspricht. In der Regel ist der Standardwert 16 ausreichend.Wenn nicht-Standardwerte verwendet werden, lesen Sie Argumente des Konstruktors für Ereignisschleifen (Abschnitt 7.7.).

Wenn die Coroutine die Ereignisschleifenmethode aufrufen muss ( normalerweise create_task ), wird sie durch Aufrufen von asyncio.get_event_loop () (ohne Argumente) zurückgegeben.

2.2 Koroutinen

Eine Coroutine wird wie folgt erstellt:

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

Mit einer Coroutine können andere Coroutinen mit der Anweisung await gestartet werden. Eine Coroutine muss mindestens eine wait- Anweisung enthalten. Dies bewirkt, dass die Coroutine vor dem Abschluss ausgeführt wird, bevor die Ausführung mit der nächsten Anweisung fortgesetzt wird. Betrachten Sie ein Beispiel:

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

In der ersten Zeile wird der Code für eine Verzögerungszeit angehalten, während andere Coroutinen diese Zeit für ihre Ausführung verwenden. Eine Verzögerung von 0 bewirkt, dass alle anstehenden Coroutinen in einer zyklischen Reihenfolge ausgeführt werden, bis die nächste Zeile ausgeführt wird. Siehe das Beispiel von roundrobin.py .

2.2.1. Warteschlange für die Planung einer Coroutine

  • EventLoop.create_task Argument: Auszuführende Coroutine. Der Scheduler stellt die Coroutine in eine Warteschlange, damit sie so schnell wie möglich startet. Der Aufruf von create_task wird sofort zurückgegeben. Die Koroutine im Argument wird in der Syntax des Funktionsaufrufs mit den erforderlichen Argumenten angegeben.
  • EventLoop.run_until_complete Argument: Auszuführende Coroutine. Der Scheduler stellt die Coroutine in eine Warteschlange, damit sie so schnell wie möglich startet. Die Koroutine im Argument wird in der Syntax des Funktionsaufrufs mit den erforderlichen Argumenten angegeben. Der Aufruf un_until_complete gibt zurück, wenn die Coroutine abgeschlossen ist: Diese Methode bietet eine Möglichkeit, den Scheduler zu beenden.
  • wait Argument: Eine auszuführende Coroutine, die mithilfe der Funktionsaufrufsyntax angegeben wird. Startet so schnell wie möglich eine Coroutine. Die anstehende Coroutine wird blockiert, bis eine der erwarteten Coroutinen abgeschlossen ist.

Das obige ist mit CPython kompatibel. Weitere Uasyncio- Methoden werden im Anhang (Abschnitt 2.2.3.) Erläutert.

2.2.2 Rückruffunktion starten

Rückrufe sollten Python- Funktionen sein, die so konzipiert sind, dass sie in kurzer Zeit ausgeführt werden. Dies liegt an der Tatsache, dass Coroutinen nicht für die gesamte Dauer der Ausführung einer solchen Funktion arbeiten können.

Die folgenden EventLoop- Klassenmethoden verwenden Rückrufe:

  1. call_soon - ruf so schnell wie möglich an. Argumente: Rückruf Rückruf, um auszuführen, * Argumente, auf die Positionsargumente möglicherweise ein Komma folgt.
  2. call_later - ruft nach einer Verzögerung in Sekunden an. Argumente: Verzögerung, Rückruf, * Argumente
  3. call_later_ms - ruft nach einer Verzögerung in ms auf. Argumente: Verzögerung, Rückruf, * Argumente .

 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 Hinweise

Eine Coroutine kann eine return- Anweisung mit beliebigen Rückgabewerten enthalten. So erhalten Sie diesen Wert:

 result = await my_coro () 

Eine Coroutine kann durch Methoden begrenzt werden und muss mindestens eine wait- Anweisung enthalten.

2.3 Verspätungen

Es gibt zwei Möglichkeiten, Verzögerungen in Coroutinen zu organisieren. Für längere Verzögerungen und in Fällen, in denen die Dauer nicht genau sein muss, können Sie Folgendes verwenden:

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

Während solcher Verzögerungen führt der Scheduler andere Coroutinen aus. Dies kann zu zeitlicher Unsicherheit führen, da die aufrufende Coroutine nur gestartet wird, wenn die gerade laufende ausgeführt wird. Die Verzögerungszeit hängt vom Anwendungsentwickler ab, liegt jedoch wahrscheinlich in der Größenordnung von zehn oder hundert ms. Dies wird im Abschnitt Interaktion mit Hardwaregeräten (Abschnitt 6) näher erläutert.

Mit den utime- Funktionen sleep_ms und sleep_us können sehr genaue Verzögerungen durchgeführt werden. Sie eignen sich am besten für kurze Verzögerungen, da der Scheduler während der Verzögerung keine anderen Coroutinen ausführen kann.

3.Sync

Oft muss die Synchronisation zwischen den Koroutinen sichergestellt werden. Ein häufiges Beispiel ist die Vermeidung der sogenannten "Race Conditions", wenn mehrere Coroutinen gleichzeitig Zugriff auf dieselbe Ressource benötigen. Ein Beispiel ist in astests.py enthalten und wird in der Dokumentation erläutert. Eine weitere Gefahr sind „Todesumarmungen“, wenn jede Koroutine darauf wartet, dass die andere abgeschlossen ist.

In einfachen Anwendungen kann die Synchronisierung mithilfe globaler Flags oder verwandter Variablen erfolgen. Ein eleganterer Ansatz ist die Verwendung von Synchronisationsklassen. Das Modul asyn.py bietet Mikroimplementierungen der Klassen Event, Barrier, Semaphore und Conditios , die nur für die Verwendung mit asyncio vorgesehen sind . Sie sind nicht threadorientiert und sollten nicht mit dem _thread- Modul oder dem Interrupt-Handler verwendet werden, sofern nicht anders angegeben. Die Lock- Klasse ist ebenfalls implementiert, was eine Alternative zur offiziellen Implementierung darstellt.

Ein weiteres Synchronisationsproblem tritt bei Coroutine-Produzenten und Coroutine-Konsumenten auf. Ein Coroutine-Produzent generiert Daten, die ein Coroutine-Konsument verwendet. Zu diesem Zweck stellt asyncio die Queue- Klasse bereit. Der Coroutine-Produzent stellt die Daten in die Warteschlange, während der Coroutine-Consumer auf die Fertigstellung wartet (wobei andere Operationen pünktlich geplant sind). Die Queue- Klasse bietet Garantien zum Entfernen von Elementen in der Reihenfolge, in der sie empfangen wurden. Alternativ können Sie die Barrier- Klasse verwenden, wenn die Producer-Coroutine warten muss, bis die Consumer-Coroutine bereit ist, auf die Daten zuzugreifen.

Eine kurze Übersicht über die Klassen finden Sie weiter unten. Weitere Details finden Sie in der vollständigen Dokumentation .

3.1 Sperre

Lock garantiert einen eindeutigen Zugriff auf eine gemeinsam genutzte Ressource. Im folgenden Codebeispiel wird eine Instanz der Sperrklasse Lock erstellt , die an alle Clients übergeben wird, die auf die freigegebene Ressource zugreifen möchten. Jede Coroutine versucht, die Sperre zu erfassen und hält die Ausführung an, bis sie erfolgreich ist:

 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 und Timeouts

Zum Zeitpunkt des Schreibens (5. Januar 2018) ist die Entwicklung der Klasse uasycio Lock noch nicht offiziell abgeschlossen. Wenn die Coroutine eine Zeitüberschreitung aufweist (Abschnitt 5.2.2.) , Ist die Zeitüberschreitung unwirksam, wenn auf eine Sperre gewartet wird, wenn diese ausgelöst wird. Es wird kein TimeoutError empfangen, bis es eine Sperre erhält. Gleiches gilt für das Abbrechen einer Aufgabe.

Das asyn.py- Modul bietet die Lock- Klasse, die in diesen Situationen funktioniert. Diese Implementierung der Klasse ist weniger effizient als die offizielle Klasse, unterstützt jedoch zusätzliche Schnittstellen gemäß der CPython- Version, einschließlich der Verwendung des Kontextmanagers.

3.2 Ereignis

Das Ereignis gibt einer oder mehreren Koroutinen die Möglichkeit, eine Pause einzulegen, während eine andere ein Signal für ihre Fortsetzung gibt. Eine Instanz von Event wird für alle Coroutinen verfügbar, die es verwenden:

 import asyn event = asyn.Event () 

Eine Coroutine wartet auf ein Ereignis, indem sie ein Warteereignis deklariert. Danach wird die Ausführung unterbrochen, bis andere Coroutinen event.set () deklarieren. Vollständige Informationen .

Ein Problem kann auftreten, wenn event.set () in einem Schleifenkonstrukt ausgegeben wird. Der Code muss warten, bis alle ausstehenden Objekte Zugriff auf das Ereignis haben, bevor er erneut festgelegt wird. Wenn ein Coro ein Ereignis erwartet, kann dies erreicht werden, indem ein Coro- Ereignis empfangen wird, das das Ereignis löscht :

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

Die Coroutine, die das Ereignis auslöst, überprüft, ob es gewartet wurde:

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

Wenn mehrere Coros auf die Synchronisation eines Ereignisses warten, kann das Problem mit dem Bestätigungsereignis behoben werden. Jeder Coro benötigt ein eigenes Event.

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

Ein Beispiel hierfür finden Sie in der Funktion event_test in der Datei asyntest.py . Dies ist in den meisten Fällen umständlich. Selbst bei einem wartenden Coro bietet die unten dargestellte Barrier- Klasse einen einfacheren Ansatz.
Ein Ereignis kann auch ein Kommunikationsmittel zwischen dem Interrupt-Handler und coro darstellen . Der Handler wartet die Hardware und setzt das Ereignis, das von coro bereits im normalen Modus geprüft wird.

3.2.1 Ereigniswerte

Die event.set () -Methode kann einen optionalen Datenwert eines beliebigen Typs annehmen. Coro wartet auf ein Ereignis und kann es mit event.value () abrufen . Beachten Sie, dass event.clear () auf None gesetzt wird . Eine typische Verwendung für die Coro- Einstellung des Ereignisses ist die Ausgabe von event.set (utime.ticks_ms ()) . Jeder Coro, der auf ein Ereignis wartet, kann die aufgetretene Verzögerung ermitteln, um dies beispielsweise zu kompensieren.

3.3 Barriere

Für die Barrier- Klasse gibt es zwei Verwendungszwecke.

Erstens kann eine Coroutine ausgesetzt werden, bis eine oder mehrere andere Coroutinen abgeschlossen sind.

Zweitens können sich mehrere Koroutinen an einem bestimmten Punkt treffen. Beispielsweise können ein Produzent und ein Konsument an dem Punkt synchronisieren, an dem der Produzent über Daten verfügt, und der Konsument ist bereit, diese zu verwenden. Zum Zeitpunkt der Ausführung kann die Barriere einen zusätzlichen Rückruf ausgeben, bevor die Barriere entfernt wird, und alle anstehenden Ereignisse können fortgesetzt werden.

Der Rückruf kann eine Funktion oder eine Coroutine sein. In den meisten Anwendungen wird die Funktion höchstwahrscheinlich verwendet: Es kann garantiert werden, dass sie vor Abschluss ausgeführt wird, bevor die Barriere entfernt wird.

Ein Beispiel ist die Funktion barrier_test in asyntest.py . Im Code-Snippet dieses Programms:

 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 

Mehrere Instanzen der Berichtskoroutine drucken ihr Ergebnis und halten an, bis auch andere Instanzen abgeschlossen sind, und warten, bis die Barriere fortgesetzt wird. An dieser Stelle wird ein Rückruf durchgeführt. Nach Fertigstellung wird die ursprüngliche Coroutine fortgesetzt.

3.4 Semaphor

Das Semaphor begrenzt die Anzahl der Coroutinen, die auf die Ressource zugreifen können. Es kann verwendet werden, um die Anzahl der Instanzen einer bestimmten Coroutine zu begrenzen, die gleichzeitig ausgeführt werden können. Dies geschieht mit einem Zugriffszähler, der vom Konstruktor initialisiert und jedes Mal reduziert wird, wenn die Coroutine ein Semaphor empfängt.

Der einfachste Weg, es in einem Kontextmanager zu verwenden:

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

Ein Beispiel ist die Funktion semaphore_test in asyntest.py .

3.4.1 ( Eingeschränktes ) Semaphor

Es funktioniert ähnlich wie die Semaphore- Klasse, außer dass ein ValueError gesetzt wird , wenn die release- Methode bewirkt, dass der Zugriffszähler seinen Anfangswert überschreitet.

3.5 Warteschlange

Die Queue- Klasse wird vom offiziellen uasycio verwaltet und das Beispielprogramm aqtest.py demonstriert seine Verwendung. Die Warteschlange wird wie folgt erstellt:

 from uasyncio.queues import Queue q = Queue () 

Eine typische Hersteller-Coroutine kann wie folgt arbeiten:

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

und die Consumer Coroutine kann wie folgt arbeiten:

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

Die Queue- Klasse bietet erhebliche zusätzliche Funktionen, wenn die Größe der Warteschlangen begrenzt und der Status abgefragt werden kann. Das Verhalten mit einer leeren Warteschlange (wenn die Größe begrenzt ist) und das Verhalten mit einer vollen Warteschlange können gesteuert werden.Die Dokumentation dazu befindet sich im Code.

3.6 Andere Synchronisationsklassen

Die Bibliothek asyn.py bietet eine Mikroimplementierung einiger anderer Funktionen von CPython .

Mit der Condition- Klasse kann eine Coroutine andere Coroutines benachrichtigen, die auf eine gesperrte Ressource warten. Nach Erhalt der Benachrichtigung erhalten sie Zugriff auf die Ressource und werden freigeschaltet. Eine Benachrichtigungskoroutine kann die Anzahl der zu benachrichtigenden Koroutinen begrenzen.

Mit der Gather- Klasse können Sie eine Liste von Coroutinen ausführen. Nach Abschluss des letzteren wird eine Liste der Ergebnisse zurückgegeben. Diese "Mikro" -Implementierung verwendet eine andere Syntax. Zeitüberschreitungen können auf jede der Coroutinen angewendet werden.

4 Entwickeln von Klassen für Asyncio

Im Rahmen der Entwicklung von Gerätetreibern soll sichergestellt werden, dass diese nicht blockieren. Ein Coroutine-Treiber muss sicherstellen, dass andere Coroutinen ausgeführt werden, während der Treiber darauf wartet, dass das Gerät Hardwarevorgänge ausführt. Beispielsweise sollte eine Task, die auf Daten wartet, die in UART eingehen, oder ein Benutzer, der eine Taste drückt, ermöglichen, dass andere Ereignisse geplant werden, bis das Ereignis eintritt.

4.1 Klassen mit Warten auf Warten Eine Coroutine

kann die Ausführung anhalten, während sie auf ein wartendes Objekt wartet . Unter CPython benutzerdefinierte Klasse awaitable durch die Implementierung einer speziellen Methode erstellt __await__was der Generator zurückgibt. Die erwartete Klasse wird wie folgt verwendet:

 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()) 

Derzeit MicroPython nicht unterstützen __await__ ( Ausgabe # 2678 ) und für die Lösung verwendet werden soll __iter__ . Die Zeichenfolge __iter__ = __await__ bietet Portabilität zwischen CPython und MicroPython . Code - Beispiele finden Sie in den Klassen Ereignis, Barrier, Cancellable, Zustand in asyn.py .

4.1.1 Verwendung in Kontextmanagern

Erwartete Objekte können in synchronen oder asynchronen Kontextmanagern verwendet werden, wobei die erforderlichen speziellen Methoden bereitgestellt werden. Syntax:

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

Um dies zu erreichen, muss sich der __await__- Generator selbst zurückgeben . Dies wird an jede Variable in der as- Klausel übergeben und ermöglicht auch die Verwendung spezieller Methoden. Siehe asyn.Condition und asyntest.condition_test, in denen die von der Condition- Klasse verwendeten Funktionen warten und in einem synchronen Kontext-Manager verwendet werden können.

4.1.2 Await Koroutine in

Sprache Python erfordert __await__ die Generatorfunktion war. In MicroPython sind die Generatoren und Coroutinen identisch, daher besteht die Lösung darin, die Ausbeute aus coro (args) zu verwenden .

Der Zweck dieses Handbuchs besteht darin, Code anzubieten, der auf CPython 3.5 oder höher portierbar ist . In CPython haben Generatoren und Coroutinen unterschiedliche Bedeutungen. In CPython verfügt eine Coroutine über eine spezielle Methode __await__ , die der Generator abruft. Dies ist portabel:

 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()) 

Beachten Sie, dass __await__, yield from asyncio.sleep (1) von CPython erlaubt ist . Ich verstehe immer noch nicht, wie dies erreicht wird.

4.2 Asynchrone Iteratoren

Asynchrone Iteratoren bieten die Möglichkeit, eine endliche oder unendliche Folge von Werten zurückzugeben. Sie können zum Abrufen von sequentiellen Datenelementen verwendet werden, wenn diese von einem schreibgeschützten Gerät stammen. Ein asynchroner Iterator ruft bei seiner nächsten Methode asynchronen Code auf . Die Klasse muss die folgenden Anforderungen erfüllen:

  • Es gibt eine __aiter__- Methode, die in async def definiert ist und einen asynchronen Iterator zurückgibt .
  • Es gibt eine __anext__- Methode , die selbst eine Coroutine ist, dh über async def definiert ist und mindestens eine wait- Anweisung enthält . Um die Iteration zu stoppen, muss eine StopAsyncIteration- Ausnahme ausgelöst werden .

Serielle Werte werden mit Async wie folgt abgerufen :

 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 Asynchrone Kontextmanager

Klassen können so entworfen werden, dass sie asynchrone Kontextmanager unterstützen, bei denen es sich um Co-Programme handelt. Ein Beispiel ist ein Klasse - Verschluss , wie oben beschrieben. Es verfügt über die Coroutine __aenter__ , die für den asynchronen Betrieb logisch erforderlich ist. Um das asynchrone Protokoll des Kontextmanagers zu unterstützen , muss die Methode __aexit__ ebenfalls eine Coroutine sein. Dies wird durch Einschließen von await asyncio.sleep (0) erreicht . Auf solche Klassen kann innerhalb einer Coroutine mit der folgenden Syntax zugegriffen werden:

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

Wie bei normalen Kontextmanagern wird die Exit-Methode garantiert aufgerufen, wenn der Kontextmanager seine Arbeit wie gewohnt und über eine Ausnahme abgeschlossen hat. Um dieses Ziel zu erreichen, werden spezielle Methoden __aenter__ und __aexit__ verwendet , die als Coroutinen definiert werden müssen, die auf eine andere Coroutine oder ein anderes wartbares Objekt warten . Dieses Beispiel stammt aus der Lock- Klasse :

  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) 

Wenn async with eine Klausel as variable enthält , erhält die Variable den von __aenter__ zurückgegebenen Wert .

Um ein korrektes Verhalten zu gewährleisten, muss die Firmware V1.9.10 oder höher sein.

5. Ausnahmen von Zeitüberschreitungen und aufgrund des Abbruchs von Aufgaben

Diese Themen stehen in Zusammenhang: uasyncio umfasst das Abbrechen von Aufgaben und das Anwenden einer Zeitüberschreitung auf eine Aufgabe, wobei eine Ausnahme für die Aufgabe auf besondere Weise ausgelöst wird .

5.1 Ausnahmen

(exeption), , , . , . , , , . , , , loop.create_task() .

throw close , . uasyncio , , , , .

Das obige Beispiel veranschaulicht diese Situation. Wenn es erlaubt ist, bis zum Ende zu arbeiten, funktioniert es wie erwartet.

 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()) 

Das Ausgeben einer Tastaturunterbrechung führt jedoch dazu, dass die Ausnahme in die Ereignisschleife eintritt. Dies liegt daran, dass die Ausführung von uasyncio.sleep an die Ereignisschleife übergeben wird. Daher müssen Anwendungen, die als Reaktion auf eine Tastaturunterbrechung einen eindeutigen Code benötigen, eine Ausnahme auf der Ebene der Ereignisschleife abfangen.

5.2 Stornierung und Timeouts

Wie oben erwähnt, sind diese Funktionen arbeiten, eine Ausnahme für eine bestimmte Aufgabe zu verursachen, eine spezielle Methode mit MicroPython Koroutine pend_throw . Wie es funktioniert, hängt von der Version ab. In der offiziellen Version 2.0 von uasyncio wird eine Ausnahme erst bei der nächsten geplanten Aufgabe verarbeitet. Dies führt zu einer Verzögerung, wenn die Aufgabe Schlaf erwartetEingabe-Ausgabe Zeitüberschreitungen können über den nominalen Zeitraum hinausgehen. Die Aufgabe zum Rückgängigmachen anderer Aufgaben kann nicht bestimmen, wann das Rückgängigmachen abgeschlossen ist.

Derzeit gibt es eine Problemumgehung und zwei Lösungen.

  • Umgehung : In der asyn- Bibliothek können Sie darauf warten, dass Aufgaben oder Aufgabengruppen abgebrochen werden. Siehe Abbrechen eines Jobs (Abschnitt 5.2.1.).
  • Die Paul Sokolovsky-Bibliothek stellt uasyncio v2.4 zur Verfügung , erfordert jedoch die Pycopy- Firmware .
  • Fast_io Bibliothek uasyncio löst dieses Problem in dem Python (weniger elegante ArtWeise) und offizielle Firmware läuft.

Die hier verwendete Ausnahmehierarchie lautet Exception-CanceledError-TimeoutError .

5.2.1 Abbrechen eines Jobs

uasyncio bietet eine Abbruchfunktion (Coro) . Dies funktioniert, indem eine Ausnahme ausgelöst wird , um die Coroutine pend_throw zu verwenden . Es funktioniert auch mit verschachtelten Coroutinen. Verwendung ist wie folgt:

 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) 

Wenn dieses Beispiel unter uasyncio v2.0 ausgeführt wird und der Balken cancel zurückgibt , wird er erst beim nächsten geplanten Foo wirksam, und es kann zu einer Verzögerung von bis zu 10 Sekunden kommen, wenn foo annulliert wird . Eine weitere Verzögerungsquelle tritt auf, wenn foo auf E / A wartet. Wo immer die Verzögerung auftritt, kann der Balken nicht feststellen, ob foo annulliert wurde. Es ist in einigen Anwendungsfällen von Bedeutung.

Bei Verwendung der Bibliotheken Paul Sokolovsky oder fast_io ist es ausreichend, sleep (0) zu verwenden:

 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) #    

Dies funktioniert auch in uasyncio v2.0, wenn foo (und alle ausstehenden Coroutine foo ) niemals den Schlaf wiedergegeben haben und nicht auf I / O gewartet haben.

Ein Verhalten, das die Unachtsamkeit überraschen kann, tritt auf, wenn erwartet wird, dass eine von create_task ausgeführte Coroutine im Standby-Modus abgebrochen wird . Betrachten Sie dieses Snippet:

 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) 

Wenn foo abgebrochen wird, wird es aus der Scheduler-Warteschlange entfernt. Da es keine return- Anweisung gibt, wird die aufrufende Prozedur foo_runner nie fortgesetzt . Es wird empfohlen, dass Sie die Ausnahme immer im äußersten Bereich der rückgängig zu machenden Funktion abfangen:

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

In diesem Fall muss my_coro die Ausnahme nicht abfangen, da sie an den aufrufenden Kanal weitergegeben und dort erfasst wird.

HinweisEs ist verboten, Close- oder Throw- Methoden von Coroutinen zu verwenden, wenn Coroutinen außerhalb des Schedulers verwendet werden. Dies untergräbt den Scheduler und zwingt die Coroutine, Code auszuführen, auch wenn er nicht geplant ist. Dies kann unerwünschte Folgen haben.

5.2.2 Coroutinen mit Timeouts

Timeouts werden mit den uasyncio- Methoden .wait_for () und .wait_for_ms () implementiert . Sie nehmen Coroutine und Latenz in Sekunden bzw. ms als Argumente. Wenn das Timeout abläuft, wird mit pend_throw ein TimeoutError in die Coroutine geworfen. Diese Ausnahme muss entweder vom Benutzer oder vom Anrufer abgefangen werden. Dies ist aus dem oben beschriebenen Grund erforderlich: Wenn das Timeout abläuft, wird es abgebrochen. Wenn der Fehler nicht abgefangen und zurückgegeben wird, kann der Aufrufer nur die Ausnahme selbst abfangen.

Wo die Ausnahme von der Coroutine abgefangen wurde, hatte ich unklare Fehler, wenn die Ausnahme nicht im äußeren Bereich abgefangen wurde, wie unten gezeigt:

 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()) 

Alternativ können Sie die aufrufende Funktion abfangen:

 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()) 

Hinweis für Uasyncio v2.0 .

Dies gilt nicht für die Bibliotheken Paul Sokolovsky oder fast_io .

Wenn die Coroutine startet und mit einer langen Verzögerung t auf asynchronen Schlaf (t) wartet , wird die Coroutine nicht neu gestartet , bis t abläuft . Wenn die Zeitüberschreitung vor dem Ende des Ruhezustands abgelaufen ist , tritt ein TimeoutError auf, wenn die Coroutine neu geladen wird - d. H. wenn t abläuft . In Echtzeit und aus Sicht des Anrufers wird seine TimeoutError- Antwort verzögert.

Wenn dies für die Anwendung wichtig ist, erstellen Sie eine lange Verzögerung, während Sie auf eine kurze Verzögerung in der Schleife warten. Coroutineasyn.sleep unterstützt dies.

6 Interaktion mit Geräten

Die Grundlage für die Interaktion zwischen uasyncio und externen asynchronen Ereignissen ist die Abfrage. Hardware, die eine schnelle Reaktion erfordert, verwendet möglicherweise einen Interrupt. Die Interaktion zwischen der Interruptroutine (ISR) und der Benutzer-Coroutine basiert jedoch auf Umfragen. Beispielsweise kann ein ISR ein Ereignis aufrufen oder ein globales Flag setzen, während eine Coroutine, die auf ein Ergebnis wartet, jedes Mal ein Objekt abfragt, wenn eine Anforderung geplant ist.

Die Abfrage kann auf zwei Arten erfolgen, explizit oder implizit. Letzteres erfolgt über Stream I / OEin Mechanismus, der für das Streaming von Geräten wie UART und Sockets entwickelt wurde . In der einfachsten expliziten Abfrage kann der folgende Code bestehen:

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

Anstelle eines globalen Flags können Sie eine Instanzvariable der Event- Klasse oder eine Instanz einer Klasse verwenden, die wait verwendet . Eine explizite Umfrage wird unten diskutiert.

Implizites Polling besteht darin, einen Treiber zu entwickeln, der als Streaming-E / A-Gerät fungiert, z. B. als UART- oder Streaming-E / A- Socket , der Geräte mit dem Python- System select.poll abfragt : Da das Polling in C ausgeführt wird, ist es schneller und effizienter als explizite Umfrage. Die Verwendung von Stream-E / A wird in Abschnitt 6.3 erläutert.

Aufgrund seiner Effektivität bietet implizites Polling den schnellsten E / A-Gerätetreibern einen Vorteil: Streaming-Treiber können für viele Geräte erstellt werden, die normalerweise nicht als Streaming-Geräte betrachtet werden. Dies wird in Abschnitt 6.4 näher erläutert.

6.1 Synchronisierungsprobleme

Sowohl explizite als auch implizite Umfragen basieren derzeit auf einer zyklischen Planung. Angenommen, E / A funktioniert gleichzeitig mit N benutzerdefinierten Coroutinen, von denen jede ohne Verzögerung ausgeführt wird. Wenn die E / A bedient wird, wird sie abgefragt, sobald alle Benutzervorgänge geplant sind. Die geschätzte Verzögerung sollte bei der Planung berücksichtigt werden. E / A-Kanäle müssen möglicherweise gepuffert werden, wobei ISR-Wartungsgeräte in Echtzeit Puffer und Coroutinen verwenden, um die Puffer zu einem langsameren Zeitpunkt zu füllen oder freizugeben.

Man muss auch die Möglichkeit eines Überschreitens in Betracht ziehen: Dies ist der Fall, wenn etwas, das von der Coroutine abgefragt wird, mehr als einmal vorkommt, bevor es tatsächlich von der Coroutine geplant wird.

Ein weiteres Zeitproblem ist die Latenzgenauigkeit. Wenn die Coroutine Probleme hat

 await asyncio.sleep_ms ( t ) #   

Der Scheduler garantiert, dass die Ausführung für mindestens t ms unterbrochen wird. Die tatsächliche Verzögerung kann größer als t sein, was von der aktuellen Systemlast abhängt. Wenn zu diesem Zeitpunkt andere Coroutinen auf die Beendigung von Verzögerungen ungleich Null warten, wird die Ausführung der nächsten Zeile sofort eingeplant. Wenn jedoch auch andere Coroutinen auf die Ausführung warten (entweder weil sie eine Verzögerung von Null ausgegeben haben oder weil ihre Zeit ebenfalls abgelaufen ist), ist möglicherweise eine frühere Ausführung geplant. Dies führt eine Synchronisationsunsicherheit in die Funktionen sleep () und sleep_ms () ein . Der Worst-Case-Wert für diesen Überlauf kann berechnet werden, indem die Laufzeitwerte aller dieser Coroutinen summiert werden, um die Worst-Case-Übertragungszeit an den Scheduler zu bestimmen.

Die fast_io- Version von uasyncio bietet in diesem Kontext eine Möglichkeit, um sicherzustellen, dass Streaming-E / A bei jeder Iteration des Schedulers abgefragt werden. Es wird gehofft, dass das offizielle uasyncio die entsprechenden Änderungen rechtzeitig akzeptiert.

6.2 Abfragen von Geräten mit Coroutinen

Dies ist ein einfacher Ansatz, der am besten für Geräte geeignet ist, die mit einer relativ geringen Geschwindigkeit abgefragt werden können. Dies ist hauptsächlich auf die Tatsache zurückzuführen, dass das Abrufen mit einem kurzen (oder Null-) Abfrageintervall dazu führen kann, dass die Coroutine mehr Prozessorzeit verbraucht, als für das Fallen in das Intervall wünschenswert ist.

Das apoll.py- Beispiel veranschaulicht diesen Ansatz durch Abfragen des Pyboard- Beschleunigungsmessersmit einem Intervall von 100 ms. Es führt eine einfache Filterung durch, um Rauschen zu ignorieren, und druckt alle zwei Sekunden eine Meldung, wenn keine Bewegung auftritt.

Das Beispiel aswitch.py enthält Treiber für Schalter und Tastengeräte .

Ein Beispieltreiber für ein Gerät, das lesen und schreiben kann, ist unten dargestellt. Zum leichteren Testen emuliert Pyboard UART 4 ​​ein bedingtes Gerät. Der Treiber implementiert die RecordOrientedUart- Klassewobei Daten in Datensätzen variabler Länge geliefert werden, die aus Byte-Instanzen bestehen. Das Objekt fügt vor dem Senden ein Trennzeichen hinzu und puffert die eingehenden Daten, bis ein hinzugefügtes Trennzeichen empfangen wird. Dies ist nur eine Demo und eine ineffiziente Art, UART im Vergleich zu Streaming Input / Output zu verwenden.

Um die asynchrone Übertragung zu demonstrieren, wird davon ausgegangen, dass das emulierte Gerät über ein Mittel verfügt, mit dem überprüft werden kann, ob die Übertragung abgeschlossen ist und die Anwendung eine Wartezeit erfordert. In diesem Beispiel ist keine der Annahmen wahr, aber der Code täuscht sie vor, indem er auf asyncio.sleep (0.1) wartet .

Vergessen Sie zu Beginn nicht, die Ausgänge des Pyboard X1 und X2 (UART Txd und Rxd) anzuschließen.

 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 Verwenden des Streaming-Mechanismus ( Stream )

Das Beispiel zeigt die gleichzeitige E / A auf einem einzelnen UART Pyboard- Mikroprozessor .

Verbinden Sie zum Starten die Ausgänge des Pyboard X1 und X2 (UART Txd und 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() 

Der unterstützende Code befindet sich in __init__.py in der uasyncio- Bibliothek. Der Mechanismus funktioniert, weil der Gerätetreiber (in C geschrieben ) die folgenden Methoden implementiert: ioctl, read, readline und write . In Abschnitt 6.4: Schreiben eines Streaming-Gerätetreibers erfahren Sie, wie solche Treiber in Python geschrieben werden können .

UART . - , . , ; , . , UART , , . , UART, . , .

6.3.1 UART

auart_hd.pyveranschaulicht ein Kommunikationsverfahren mit einem Halbduplex-Gerät, beispielsweise einem Gerät, das auf den AT-Modem-Befehlssatz reagiert. Halbduplex bedeutet, dass das Gerät niemals unerwünschte Daten sendet: Die Übertragung erfolgt immer auf einen empfangenen Befehl des Masters hin.

Das Gerät wird durch Ausführen eines Tests auf einem Pyboard mit zwei Kabelverbindungen emuliert .

Das (sehr vereinfachte) emulierte Gerät reagiert auf jeden Befehl, indem es vier Datenzeilen mit einer Pause dazwischen sendet, um eine langsame Verarbeitung zu simulieren.

Der Assistent sendet einen Befehl, weiß jedoch nicht im Voraus, wie viele Datenzeilen zurückgegeben werden. Es startet einen Neustart-Timer, der jedes Mal neu startet, wenn eine Leitung empfangen wird. Nach Ablauf des Timers wird davon ausgegangen, dass das Gerät die Übertragung abgeschlossen hat und eine Liste der empfangenen Leitungen zurückgegeben wird.

Es wird auch ein Geräteausfall beschrieben, bei dem eine Übertragung übersprungen wird, bevor auf eine Antwort gewartet wird. Nach dem Timeout wird eine leere Liste zurückgegeben. Weitere Details finden Sie in den Codekommentaren.

6.4 Entwicklung von Streaming (Treiber - Stream ) -Einheit

Strom Ein- / Ausgabemechanismus ( Stream - E / A ) zur Steuerung des Betriebes von Streaming - E / A - Geräten wie UART und Buchsen ( Steckdose) Der Mechanismus kann von Treibern jedes regelmäßig abgefragten Geräts verwendet werden, indem er an den Scheduler delegiert wird, der select verwendet, um die Bereitschaft aller Geräte in der Warteschlange abzufragen. Dies ist effizienter als die Ausführung mehrerer Coroutine-Operationen, von denen jede das Gerät abfragt , zum Teil, weil select in C geschrieben ist , und auch, weil die Coroutine, die die Abfrage durchführt, verzögert wird, bis das abgefragte Objekt einen Bereitschaftszustand zurückgibt.

Ein Gerätetreiber, der den Streaming-Ein- / Ausgabemechanismus bedienen kann, sollte vorzugsweise die Methoden StreamReader, StreamWriter unterstützen. Ein lesbares Gerät muss mindestens eine der folgenden Methoden bereitstellen. Bitte beachten Sie, dass dies synchrone Methoden sind. Die ioctl- Methode (siehe unten) stellt sicher, dass sie nur aufgerufen werden, wenn Daten verfügbar sind. Methoden sollten so schnell wie möglich zurückgegeben werden, wobei so viele Daten wie möglich verwendet werden.

readline () Gibt so viele Zeichen wie möglich bis zu einem beliebigen Zeilenvorschub zurück. Erforderlich bei Verwendung von StreamReader.readline ()

read (n) Gibt so viele Zeichen wie möglich zurück, jedoch nicht mehr als n . Erforderlich, wenn StreamReader.read () oder StreamReader.readexactly () verwendet wird

Der erstellte Treiber sollte die folgende synchrone Methode mit sofortiger Rückgabe bereitstellen:

Schreiben Sie mit den Argumenten buf, off, sz .

Wo:

buf ist ein Puffer zum Schreiben.
off - Offset zum Puffer des ersten zu schreibenden Zeichens.
sz - Die angeforderte Anzahl der zu schreibenden Zeichen.
Der Rückgabewert ist die Anzahl der tatsächlich geschriebenen Zeichen (möglicherweise 1, wenn das Gerät langsam ist).
Die ioctl- Methode stellt sicher, dass sie nur aufgerufen wird, wenn das Gerät bereit ist, Daten zu empfangen.

Alle Geräte müssen eine ioctl- Methode bereitstellen, mit der Geräte abgefragt werden , um ihren Verfügbarkeitsstatus zu ermitteln. Ein typisches Beispiel für einen Lese- / Schreibtreiber:

 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 

Im Folgenden wird die Wartezeit der MillisecTimer- Klasse beschrieben :

 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 

welches wie folgt verwendet werden kann:

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

Gegenüber dem offiziellen uasyncio bietet eine solche Implementierung keine Vorteile gegenüber dem asyncio.sleep_ms () . Die Verwendung von fast_io bietet im normalen Verwendungsmuster wesentlich genauere Verzögerungen, wenn Coroutinen eine Verzögerung von Null erwarten.

Sie können die E / A-Planung verwenden, um ein Ereignis einem Rückruf zuzuordnen. Dies ist effizienter als der Abrufzyklus , da der Abruf erst geplant wird, wenn ioctl bereit ist. Als nächstes wird ein Rückruf ausgeführt, wenn der Rückruf den Zustand ändert.

 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 

Und wieder - auf der offiziellen Uasyncio kann die Verzögerung hoch sein. Je nach Anwendungsdesign ist die fast_io- Version möglicherweise effizienter.

Die iorw.py-Demo zeigt ein vollständiges Beispiel. Bitte beachten Sie, dass zum Zeitpunkt des Schreibens des Artikels im offiziellen uasyncio ein Fehler vorliegt, aufgrund dessen dies nicht funktioniert . Es gibt zwei Lösungen. Die Problemumgehung besteht darin, zwei separate Treiber zu schreiben, einen nur zum Lesen und einen nur zum Schreiben. Die zweite Möglichkeit ist die Verwendung von fast_io , um dieses Problem zu lösen.

Im offiziellen Uasyncio ist die Eingabe / Ausgabe ziemlich selten geplant .

6.5 Vollständiges Beispiel: aremote.py

Der Treiber ist zum Empfangen / Dekodieren von Signalen von einer Infrarot-Fernbedienung ausgelegt. Der aremote.py- Treiber selbst . Die folgenden Hinweise sind für die Verwendung von Asyncio von Bedeutung .

Die Unterbrechung des Kontakts zeichnet den Zeitpunkt des Zustandswechsels (in Mikrosekunden) auf und setzt das Ereignis, wobei der Zeitpunkt des ersten Zustandswechsels übersprungen wird. Die Coroutine wartet auf ein Ereignis, meldet die Dauer des Datenpakets und decodiert dann die gespeicherten Daten, bevor sie den vom Benutzer angegebenen Rückruf aufruft.

Durchdie Übergabe der Zeit an eine Ereignisinstanz kann die Coroutinebeim Festlegen der Verzögerungszeitjede Asynchronitätsverzögerung ausgleichen.

6.6 Umgebungssensor HTU21D

Der HTU21D-Chiptreiber bietet genaue Temperatur- und Feuchtigkeitsmessungen.

Der Chip benötigt ca. 120 ms, um beide Datenelemente zu empfangen. Der Treiber arbeitet asynchron und initiiert den Empfang und die Verwendung von wait asyncio.sleep (t), bevor Daten gelesen werden. Er aktualisiert die Temperatur- und Feuchtigkeitsvariablen, auf die jederzeit zugegriffen werden kann. Dadurch können andere Coroutinen gestartet werden, während der Chiptreiber ausgeführt wird.

7. Tipps und Tricks

7.1 Das Programm friert ein Das Einfrieren erfolgt

normalerweise, weil die Aufgabe ohne Erlaubnis blockiert wird. Dies führt zum Einfrieren des gesamten Systems. Bei der Entwicklung ist es nützlich, eine Coroutine zu haben, die die eingebaute LED regelmäßig einschaltet. Dadurch wird bestätigt, dass der Scheduler noch ausgeführt wird.

7.2 uasyncio speichert Status

Wenn Sie Programme mit uasyncio in REPL starten , führen Sie zwischen den Starts einen Soft-Reset (Strg-D) durch. Aufgrund der Tatsache, dass uasyncio den Status zwischen den Starts beibehält, kann es beim nächsten Start zu unvorhersehbarem Verhalten kommen.

7.3 Speicherbereinigung

Sie können eine Coroutine ausführen, indem Sie zuerst import gc angeben :

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

Der Zweck hierfür wird hier im Abschnitt Heap erläutert .

7.4 Testen

Es wird empfohlen, sicherzustellen, dass der Gerätetreiber die Kontrolle behält, wenn dies erforderlich ist. Führen Sie dazu eine oder mehrere Kopien von fiktiven Coroutinen aus, die den Nachrichtendruckzyklus starten, und überprüfen Sie, ob der Treiber in den folgenden Zeiträumen im Standby-Modus ausgeführt wird:

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

Als ein Beispiel die Art der Gefahr , die im obigen Beispiel auftreten kann RecordOrientedUart __await__ wurde Methode ursprünglich geschrieben als:

 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 

Infolgedessen wird die Ausführung so lange gestreckt, bis der gesamte Datensatz empfangen wurde. Außerdem gibt uart.any () immer eine von Null verschiedene Anzahl empfangener Zeichen zurück. Zum Zeitpunkt des Anrufs sind möglicherweise bereits alle Zeichen eingegangen. Diese Situation kann mit einer externen Schleife gelöst werden:

 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 

Es kann erwähnenswert sein, dass dieser Fehler nicht offensichtlich gewesen wäre, wenn die Daten mit einer geringeren Geschwindigkeit an den UART gesendet worden wären, anstatt einen Rückkopplungstest zu verwenden. Willkommen zu den Freuden der Echtzeitprogrammierung.

7.5 Häufiger Fehler

Wenn eine Funktion oder Methode von async def definiert und anschließend wie ein regulärer (synchroner) Aufruf aufgerufen wird, zeigt MicroPython keine Fehlermeldung an. Dies ist beabsichtigt. Normalerweise führt dies dazu, dass das Programm im Hintergrund nicht richtig funktioniert:

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

Ich habe einen Vorschlag , der vorschlägt, die Situation in Option 1 mit fast_io zu beheben .

Das Modul check_async_code.py versucht, Fälle von zweifelhafter Verwendung von Coroutinen zu erkennen. Es ist in Python3 geschrieben und für die Arbeit auf einem PC ausgelegt. Wird in Skripten verwendet, die gemäß den in diesem Handbuch beschriebenen Richtlinien mit Coroutinen geschrieben wurden, die mit async def deklariert wurden . Das Modul hat ein Argument, den Pfad zu der Quelldatei MicroPython (oder --help).

Bitte beachten Sie, dass es etwas unhöflich ist und in einer syntaktisch korrekten Datei verwendet werden soll, die nicht standardmäßig gestartet wird. Verwenden Sie ein Tool wie pylint zur allgemeinen Syntaxprüfung (bei pylint tritt dieser Fehler derzeit nicht auf).

Das Skript erzeugt falsche Positive. Koroutinen sind laut Plan Objekte der ersten Ebene, sie können auf Funktionen übertragen und in Datenstrukturen abgelegt werden. Abhängig von der Logik des Programms können Sie die Funktion oder das Ergebnis ihrer Ausführung speichern. Das Skript kann die Absicht nicht bestimmen. Es zielt darauf ab, Fälle zu ignorieren, die korrekt erscheinen, wenn andere zu berücksichtigende Fälle identifiziert werden. Angenommen, foo, in der die Coroutine als asynchron def deklariert ist :

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

Ich finde es nützlich, aber Verbesserungen sind immer willkommen.

7.6 Programmieren mit Steckdosen ( Steckdosen )

Es gibt zwei grundlegende Ansätze zur Programmierung Steckdosen uasyncio . Standardmäßig werden Sockets gesperrt, bis der angegebene Lese- oder Schreibvorgang abgeschlossen ist. Uasyncio unterstützt das Sperren von Sockets mit select.poll , um zu verhindern, dass der Scheduler sie blockiert. In den meisten Fällen ist dieser Mechanismus am einfachsten zu bedienen. Ein Beispiel für Client- und Server-Code finden Sie im Verzeichnis client_server . Der Benutzer verwendet die Anwendung select.poll , indem er den Server-Socket explizit abfragt.

Client-Sockets verwenden es implizit in dem Sinne, dass die uasyncio- Streaming-Engine es direkt verwendet.

Bitte beachten Sie, dass socket.getaddrinfo derzeit gesperrt ist. Die Zeit im Beispielcode ist minimal, aber wenn eine DNS-Suche erforderlich ist, kann der Sperrzeitraum erheblich sein.

Ein zweiter Ansatz zur Socket-Programmierung ist die Verwendung nicht blockierender Sockets. Dies erhöht die Komplexität, ist jedoch in einigen Anwendungen erforderlich, insbesondere wenn die Verbindung über WLAN erfolgt (siehe unten).

Zum Zeitpunkt dieser Veröffentlichung (März 2019) befand sich die TLS-Unterstützung für nicht blockierende Sockets in der Entwicklung. Ihr genauer Status ist mir unbekannt.

Die Verwendung von nicht blockierenden Steckdosen erfordert viel Liebe zum Detail. Wenn aufgrund der Serverlatenz nicht blockierende Lesevorgänge auftreten, kann nicht garantiert werden, dass alle (oder einige) der angeforderten Daten zurückgegeben werden. Ebenso können Einträge nicht vollständig sein.

Daher müssen asynchrone Lese- und Schreibmethoden iterativ eine nicht blockierende Operation ausführen, bis die erforderlichen Daten gelesen oder geschrieben wurden. In der Praxis kann eine Zeitüberschreitung erforderlich sein, um Serverausfälle zu beheben.
Eine weitere Komplikation ist, dass der ESP32-Port Probleme hatte, die für einen fehlerfreien Betrieb ziemlich unangenehme Einbrüche erforderten. Ich habe nicht getestet, ob dies noch der Fall ist. Sock_nonblock.py-
Modulveranschaulicht die erforderlichen Methoden. Dies ist keine funktionierende Demo und die Entscheidungen sind wahrscheinlich anwendungsabhängig.

7.6.1 Probleme mit WiFi

Der uasyncio- Streaming-Mechanismus ist nicht die beste Option zum Erkennen von WiFi-Ausfällen. Ich fand es notwendig, nicht blockierende Sockets zu verwenden, um einen ausfallsicheren Betrieb zu gewährleisten und den Client bei Fehlern erneut zu verbinden.

In diesem Dokument werden die Probleme beschrieben, auf die ich in WiFi-Anwendungen gestoßen bin, die Sockets für längere Zeit offen halten, und die Lösung skizziert.

Pltcmbietet einen robusten asynchronen MQTT-Client, der die Nachrichtenintegrität bei WLAN-Ausfällen gewährleistet. Eine einfache asynchrone serielle Vollduplex-Verbindung zwischen einem drahtlosen Client und einem verdrahteten Server mit garantierter Nachrichtenübermittlung wird beschrieben.

7.7 Argumente des Konstruktors für Ereignisschleifen

Ein kleiner Fehler kann auftreten, wenn Sie eine Ereignisschleife mit Werten erstellen müssen, die von den Standardwerten abweichen. Eine solche Schleife muss deklariert werden, bevor ein anderer Code mit asyncio ausgeführt wird, da diese Werte in diesem Code möglicherweise erforderlich sind. Andernfalls wird der Code mit den Standardwerten initialisiert:

 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) 

Da beim Importieren eines Moduls Code ausgeführt werden kann, ist es am sichersten, eine Ereignisschleife unmittelbar nach dem Import von uasyncio zu instanziieren .

 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()    

Beim Schreiben von Modulen zur Verwendung durch andere Programme ziehe ich es vor, uasyncio- Code beim Import nicht auszuführen . Schreiben Sie Funktionen und Methoden, um auf eine Ereignisschleife als Argument zu warten. Stellen Sie dann sicher, dass nur Anwendungen der obersten Ebene get_event_loop aufrufen :

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

Dieses Problem wird hier diskutiert .

8 Notizen für Anfänger

Diese Notizen richten sich an Anfänger in asynchronem Code. Sie beginnen mit einer Beschreibung der Probleme, die Planer zu lösen versuchen, und geben einen Überblick über den Lösungsansatz von uasyncio .

In Abschnitt 8.5 werden die relativen Vorteile der Module uasyncio und _ thread sowie die Gründe erläutert , warum Sie uasyncio- Coroutinen mit proaktiver Zeitplanung (_thread) möglicherweise bevorzugen .

8.1 Problem 1: Ereignisschleifen

Eine typische Firmware-Anwendung arbeitet ununterbrochen und sollte gleichzeitig auf externe Ereignisse reagieren, zu denen eine Spannungsänderung am ADC, das Auftreten eines Hardware-Interrupts oder ein im UART empfangenes Symbol oder auf Daten, die auf dem Sockel verfügbar sind, gehören können. Diese Ereignisse treten asynchron auf, und der Code sollte in der Lage sein, unabhängig von der Reihenfolge, in der sie auftreten, zu reagieren. Darüber hinaus können zeitabhängige Aufgaben erforderlich sein, z. B. das Blinken von LEDs.

Die naheliegende Möglichkeit hierfür ist die uasycio- Ereignisschleife . Dieses Beispiel ist kein praktischer Code, sondern dient zur Veranschaulichung der allgemeinen Form der Ereignisschleife.

 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 

Eine solche Schleife funktioniert für einfache Beispiele, aber mit zunehmender Anzahl von Ereignissen wird der Code schnell umständlich. Sie verletzen auch die Prinzipien der objektorientierten Programmierung, indem sie den größten Teil der Programmlogik an einem Ort kombinieren, anstatt Code mit einem gesteuerten Objekt zu verknüpfen. Wir wollen eine Klasse für eine blinkende LED entwickeln, die in ein Modul eingefügt und importiert werden kann. Der OOP-Ansatz für das Blinken von LEDs könnte folgendermaßen aussehen:

 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, #          

Mit dem Scheduler in uasyncio können Sie solche Klassen erstellen.

8.2 Problem 2: Blockierungsmethoden

Angenommen, Sie müssen eine bestimmte Anzahl von Bytes aus einem Socket lesen. Wenn Sie socket.read (n) standardmäßig mit einem blockierenden Socket aufrufen , wird es "blockieren" ( dh es kann nicht beendet werden), bis n Bytes empfangen werden . Während dieser Zeit reagiert die Anwendung nicht auf andere Ereignisse.

Mit dem nicht blockierenden uasyncio- Socket können Sie eine asynchrone Lesemethode schreiben. Eine Aufgabe, für die Daten erforderlich sind, wird (notwendigerweise) blockiert, bis sie empfangen werden. In diesem Zeitraum werden jedoch andere Aufgaben ausgeführt, sodass die Anwendung weiterhin reagiert.

8.3. Uasyncio-Ansätze

Die nächste Klasse verfügt über eine LED, die ein- und ausgeschaltet werden kann. Sie kann auch bei jeder Geschwindigkeit blinken. Die LED_async- Instanz verwendet die Ausführungsmethode , die für den kontinuierlichen Betrieb verwendet werden kann. Das Verhalten von LEDs kann mit den Methoden on (), off () und flash (secs) gesteuert werden .

 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 

Es ist zu beachten, dass on (), off () und flash () normale synchrone Methoden sind. Sie ändern das Verhalten der LED, kehren aber sofort zurück. Das Blinken erfolgt "im Hintergrund". Dies wird im nächsten Abschnitt ausführlich erläutert.

Die Klasse entspricht dem OOP-Prinzip, bei dem die dem Gerät zugeordnete Logik in der Klasse gespeichert wird. Gleichzeitig stellt die Verwendung von uasyncio sicher, dass die Anwendung auf andere Ereignisse reagieren kann, während die LED blinkt. Das folgende Programm blinkt mit vier Pyboard- LEDs mit unterschiedlichen Frequenzen und reagiert auch auf die USR-Taste, die es vervollständigt.

 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()) 

Im Gegensatz zum ersten Beispiel einer Ereignisschleife befindet sich die dem Schalter zugeordnete Logik in einer Funktion, die von der Funktionalität der LED getrennt ist. Achten Sie auf den Code, der zum Starten des Schedulers verwendet wird:

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

8.4 Planung in uasyncio

Python 3.5 und MicroPython unterstützen das Konzept einer asynchronen Funktion, die auch als Coroutine oder Task bezeichnet wird. Eine Coroutine muss mindestens eine wait- Anweisung enthalten .

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

Diese Funktion druckt zehnmal im Sekundentakt eine Nachricht. Während die Funktion in Erwartung einer Verzögerung angehalten wird , führt der Asyncio-Scheduler andere Aufgaben aus, wodurch die Illusion entsteht, sie gleichzeitig auszuführen.

Wenn die Coroutine-Probleme auf asyncio.sleep_ms () oder asyncio.sleep () warten, wird die aktuelle Task angehalten und in eine Warteschlange gestellt, die nach Zeit geordnet ist, und die Ausführung wird mit der Task am Anfang der Warteschlange fortgesetzt. Die Warteschlange ist so konzipiert, dass selbst wenn der angegebene Ruhemodus Null ist, andere relevante Aufgaben ausgeführt werden, bis der Strom wieder aufgenommen wird. Dies ist eine „ehrliche Kreislaufplanung“. Es ist gängige Praxis, asyncio.sleep (0) -Schleifen abzuwarten .Damit die Task die Ausführung nicht verzögert. Die folgende Schleife wartet darauf, dass eine andere Task die globale Flag- Variable setzt . Leider monopolisiert es den Prozessor und verhindert den Start anderer Coroutinen:

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

Das Problem hierbei ist , dass keine andere Task gestartet wird, bis die Flagis-False- Schleife die Steuerung an den Scheduler übergibt . Der richtige Ansatz:

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

Aus dem gleichen Grund ist es üblich, Verzögerungen festzulegen , z. B. utime.sleep (1), da andere Tasks für 1 s blockiert werden. Es ist richtiger, wait asyncio.sleep (1) zu verwenden .
Beachten Sie, dass Verzögerungen, die von den Methoden uasyncio sleep und sleep_ms generiert werden, die angegebene Zeit überschreiten können. Dies liegt an der Tatsache, dass andere Aufgaben während der Verzögerung ausgeführt werden. Nach Ablauf der Verzögerungszeit wird die Ausführung erst fortgesetzt, wenn die ausgeführten Aufgaben warten oder beendet sind. Eine wohlerzogene Coroutine wird immer das Warten erklärenin regelmäßigen Abständen. Wenn eine genaue Verzögerung erforderlich ist, insbesondere wenn eine weniger als einige ms beträgt, muss möglicherweise utime.sleep_us (us) verwendet werden .

8.5 Warum kollaboratives, nicht threadbasiertes Scheduling ( _thread )?

Die anfängliche Reaktion von Anfängern auf die Idee, Koroutinen mitzuplanen, ist oft enttäuschend. Sicherlich ist Streaming-Planung besser? Warum sollte ich die Kontrolle explizit aufgeben, wenn die virtuelle Python-Maschine dies für mich tun kann?

Bei eingebetteten Systemen bietet das Kollaborationsmodell zwei Vorteile.
Das erste ist geringes Gewicht. Es ist möglich, dass eine große Anzahl von Coroutinen vorhanden ist, da suspendierte Coroutinen im Gegensatz zu geplanten Threads weniger Platz beanspruchen.
Zweitens werden auf diese Weise einige der subtilen Probleme im Zusammenhang mit der Streaming-Planung vermieden.

In der Praxis ist das kollaborative Multitasking weit verbreitet, insbesondere in Benutzeroberflächenanwendungen.

Zur Verteidigung des Streaming-Planungsmodells zeige ich einen Vorteil: Wenn jemand schreibt

 for x in range ( 1000000 ): #  -  

Andere Aufgaben werden nicht blockiert. Das Kollaborationsmodell geht davon aus, dass die Schleife der Steuerung jeder Aufgabe explizit eine bestimmte Anzahl von Iterationen zuweisen soll , z. B. Code in eine Coroutine einfügen und in regelmäßigen Abständen den Befehl wait asyncio.sleep (0) ausgeben soll .

Leider verblasst dieser Vorteil im Vergleich zu den Nachteilen. Einige davon sind in der Dokumentation zum Schreiben von Interrupt-Handlern beschrieben.. In einem Streaming-Planungsmodell kann jeder Thread jeden anderen Thread unterbrechen und die Daten ändern, die in anderen Threads verwendet werden können. In der Regel ist es viel einfacher, eine Sperre zu finden und zu beheben, die aufgrund eines Fehlers auftritt, der kein Ergebnis liefert, als manchmal sehr subtile und selten auftretende Fehler zu erkennen, die in Code auftreten können, der im Rahmen eines Modells mit Streaming-Planung geschrieben wurde.

Einfach ausgedrückt, wenn Sie eine MicroPython- Coroutine schreiben , können Sie sicher sein, dass die Variablen nicht plötzlich von einer anderen Coroutine geändert werden: Ihre Coroutine hat die volle Kontrolle, bis sie wieder asyncio.sleep (0) erwartet .

Denken Sie daran, dass Interrupt-Handler präventiv sind. Dies gilt sowohl für Hardware- als auch für Software-Interrupts, die an einer beliebigen Stelle in Ihrem Code auftreten können.

Eine eloquente Diskussion zu Fragen der Streaming-Planung finden Sie hier .

8.6 Interaktion

In nicht trivialen Anwendungen müssen Coroutinen interagieren. Es können herkömmliche Python- Methoden verwendet werden . Dazu gehören die Verwendung globaler Variablen oder die Deklaration von Coroutinen als Objektmethoden: Sie können Instanzvariablen gemeinsam nutzen. Alternativ kann ein veränderbares Objekt als Argument an eine Coroutine übergeben werden.

Für das Streaming-Planungsmodell müssen Spezialisten sicherstellen, dass Klassen eine sichere Verbindung bereitstellen. In einem Kollaborationsmodell ist dies selten erforderlich.

8.7. Poll ( Polling )

Einige Hardware - Gerät wie ein Beschleunigungsmesser Pyboard , unterstützen keine Unterbrechungen und daher abgefragt werden soll ( das heißt periodisch überprüft). Polling kann auch in Verbindung mit Interrupt-Handlern verwendet werden: Der Interrupt-Handler verwaltet die Ausrüstung und setzt ein Flag. Die Coroutine fragt das Flag ab - wenn es gesetzt ist, werden Daten verarbeitet und das Flag zurückgesetzt. Der beste Ansatz ist die Verwendung der Event- Klasse .

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


All Articles