Python – wie man eine C-Funktion als Awaitable (Coroutine) implementiert

Lesezeit: 7 Minuten

Benutzer-Avatar
Bob

Umgebung: Kooperatives RTOS in C und Micropython Virtual Machine ist eine der Aufgaben.

Damit die VM die anderen RTOS-Tasks nicht blockiert, füge ich ein RTOS_sleep() in vm.c:DISPATCH() so dass die VM nach Ausführung jedes Bytecodes die Kontrolle an die nächste RTOS-Aufgabe abgibt.

Ich habe eine uPy-Schnittstelle erstellt, um Daten asynchron von einem physikalischen Datenbus zu erhalten – könnte CAN, SPI, Ethernet sein – unter Verwendung des Producer-Consumer-Designmusters.

Verwendung in uPy:

can_q = CANbus.queue()
message = can_q.get()

Die Implementierung in C ist so, dass can_q.get() blockiert NICHT das RTOS: es fragt eine C-Warteschlange ab und wenn keine Nachricht empfangen wird, ruft es an RTOS_sleep() um einer anderen Aufgabe die Chance zu geben, die Warteschlange zu füllen. Die Dinge werden synchronisiert, da die C-Warteschlange nur von einem anderen RTOS-Task aktualisiert wird und RTOS-Tasks nur dann wechseln, wenn RTOS_sleep() heißt dh Kooperative

Die C-Implementierung ist im Wesentlichen:

// gives chance for c-queue to be filled by other RTOS task
while(c_queue_empty() == true) RTOS_sleep(); 
return c_queue_get_message();

Obwohl die Python-Anweisung can_q.get() blockiert nicht das RTOS, es blockiert das uPy-Skript. Ich möchte es umschreiben, damit ich es mit verwenden kann async def dh Coroutine und lassen Sie das uPy-Skript nicht blockieren.

Nicht sicher die Syntax aber sowas:

can_q = CANbus.queue()
message = await can_q.get()

FRAGE

Wie schreibe ich eine C-Funktion, damit ich kann await drauf?

Ich würde eine CPython- und Micropython-Antwort bevorzugen, aber ich würde eine Nur-CPython-Antwort akzeptieren.

Benutzer-Avatar
Benutzer4815162342

Hinweis: Diese Antwort behandelt CPython und das Asyncio-Framework. Die Konzepte sollten jedoch für andere Python-Implementierungen sowie andere asynchrone Frameworks gelten.

Wie schreibe ich eine C-Funktion, damit ich kann await drauf?

Der einfachste Weg, eine C-Funktion zu schreiben, deren Ergebnis abgewartet werden kann, besteht darin, dass sie ein bereits als erwartebar gemachtes Objekt wie z. B. an zurückgibt asyncio.Future. Vor der Rücksendung der Future, muss der Code dafür sorgen, dass das Ergebnis der Zukunft durch einen asynchronen Mechanismus festgelegt wird. Alle diese auf Koroutinen basierenden Ansätze gehen davon aus, dass Ihr Programm unter einer Ereignisschleife läuft, die weiß, wie die Koroutinen zu planen sind.

Aber eine Zukunft zurückzugeben ist nicht immer genug – vielleicht möchten wir ein Objekt mit einer beliebigen Anzahl von Aufhängungspunkten definieren. Das Zurückgeben eines Futures wird nur einmal ausgesetzt (wenn das zurückgegebene Future nicht vollständig ist), wird fortgesetzt, sobald das Future abgeschlossen ist, und das war’s. Ein erwartebares Objekt, das einem entspricht async def die mehr als einen enthält await kann nicht durch Zurückgeben einer Zukunft implementiert werden, es muss ein Protokoll implementieren, das Coroutinen normalerweise implementieren. Dies ist so etwas wie ein Iterator, der einen Custom implementiert __next__ und anstelle eines Generators verwendet werden.

Definieren eines benutzerdefinierten awaitable

Um unseren eigenen Awaitable-Typ zu definieren, können wir uns PEP 492 zuwenden, das spezifiziert genau an welche Objekte übergeben werden kann await. Andere als mit definierte Python-Funktionen async defbenutzerdefinierte Typen können Objekte erwartebar machen, indem sie die definieren __await__ spezielle Methode, die Python/C dem zuordnet tp_as_async.am_await Teil von PyTypeObject Struktur.

Das bedeutet, dass Sie in Python/C Folgendes tun müssen:

  • Geben Sie einen Nicht-NULL-Wert für an tp_as_async Feld Ihres Erweiterungstyps.
  • habe seine am_await Member zeigen auf eine C-Funktion, die eine Instanz Ihres Typs akzeptiert und eine Instanz eines anderen Erweiterungstyps zurückgibt, der die implementiert Iterator-Protokolldh definiert tp_iter (trivial definiert als PyIter_Self) und tp_iternext.
  • des Iterators tp_iternext muss die Zustandsmaschine der Coroutine vorrücken. Jede nicht außergewöhnliche Rückkehr von tp_iternext entspricht einer Aussetzung und dem Finale StopIteration Ausnahme bedeutet die endgültige Rückkehr von der Coroutine. Der Rückgabewert wird in der gespeichert value Eigentum von StopIteration.

Damit die Coroutine nützlich ist, muss sie auch in der Lage sein, mit der Ereignisschleife zu kommunizieren, die sie antreibt, damit sie angeben kann, wann sie fortgesetzt werden soll, nachdem sie ausgesetzt wurde. Die meisten von asyncio definierten Coroutinen werden voraussichtlich unter der asyncio-Ereignisschleife ausgeführt und intern verwendet asyncio.get_event_loop() (und/oder akzeptieren Sie eine explizite loop Argument), um seine Dienste zu erhalten.

Beispiel Coroutine

Um zu veranschaulichen, was der Python/C-Code implementieren muss, betrachten wir eine einfache Koroutine, die als Python ausgedrückt wird async defwie dieses Äquivalent von asyncio.sleep():

async def my_sleep(n):
    loop = asyncio.get_event_loop()
    future = loop.create_future()
    loop.call_later(n, future.set_result, None)
    await future
    # we get back here after the timeout has elapsed, and
    # immediately return

my_sleep schafft ein Futuresorgt dafür, dass es abgeschlossen wird (sein Ergebnis gesetzt wird). n Sekunden und setzt sich selbst aus, bis die Zukunft abgeschlossen ist. Der letzte Teil verwendet awaitwo await x bedeutet „zulassen x zu entscheiden, ob wir jetzt suspendieren oder weiter ausführen”. Eine unvollständige Zukunft entscheidet immer zu suspendieren, und die asyncio Task Sonderfälle von Coroutine-Treibern ergaben Futures, um sie auf unbestimmte Zeit auszusetzen, und verbinden ihre Fertigstellung mit der Wiederaufnahme der Aufgabe. Aufhängungsmechanismen anderer Ereignisschleifen (Kuriositäten usw.) können sich in Details unterscheiden, aber die zugrunde liegende Idee ist dieselbe: await ist eine fakultative Aussetzung der Vollstreckung.

__await__() das gibt einen Generator zurück

Um dies in C zu übersetzen, müssen wir die Magie loswerden async def Funktionsdefinition sowie der await Aufhängepunkt. Entferne den async def ist ziemlich einfach: Die äquivalente gewöhnliche Funktion muss einfach ein Objekt zurückgeben, das implementiert __await__:

def my_sleep(n):
    return _MySleep(n)

class _MySleep:
    def __init__(self, n):
        self.n = n

    def __await__(self):
        return _MySleepIter(self.n)

Das __await__ Methode der _MySleep Objekt zurückgegeben von my_sleep() wird automatisch von der aufgerufen await Operator zum Konvertieren von an abwarten Objekt (alles, an das übergeben wird await) an einen Iterator. Dieser Iterator wird verwendet, um das erwartete Objekt zu fragen, ob es sich entscheidet, zu suspendieren oder einen Wert bereitzustellen. Dies ist ähnlich wie die for o in x Anweisung fordert x.__iter__() die umzuwandeln wiederholbar x zu einem Beton Iterator.

Wenn der zurückgegebene Iterator aussetzt, muss er lediglich einen Wert erzeugen. Die Bedeutung des Werts, falls vorhanden, wird vom Coroutine-Treiber interpretiert, der typischerweise Teil einer Ereignisschleife ist. Wenn der Iterator die Ausführung beendet und zurückkehrt await, muss es aufhören zu iterieren. Verwenden eines Generators als Convenience-Iterator-Implementierung, _MySleepIter würde so aussehen:

def _MySleepIter(n):
    loop = asyncio.get_event_loop()
    future = loop.create_future()
    loop.call_later(n, future.set_result, None)
    # yield from future.__await__()
    for x in future.__await__():
        yield x

Wie await x Karten zu yield from x.__await__()muss unser Generator den von zurückgegebenen Iterator erschöpfen future.__await__(). Der von zurückgegebene Iterator Future.__await__ wird nachgeben, wenn die Zukunft unvollständig ist, und das Ergebnis der Zukunft zurückgeben (was wir hier ignorieren, aber yield from eigentlich sieht) nichts anderes.

__await__() die einen benutzerdefinierten Iterator zurückgibt

Das letzte Hindernis für eine C-Implementierung von my_sleep in C ist die Verwendung von Generatoren für _MySleepIter. Glücklicherweise kann jeder Generator in einen zustandsbehafteten Iterator übersetzt werden, dessen __next__ führt den Codeabschnitt bis zum nächsten await oder return aus. __next__ implementiert eine Zustandsmaschinenversion des Generatorcodes, wobei yield wird durch die Rückgabe eines Werts ausgedrückt, und return durch Erhöhung StopIteration. Zum Beispiel:

class _MySleepIter:
    def __init__(self, n):
        self.n = n
        self.state = 0

    def __iter__(self):  # an iterator has to define __iter__
        return self

    def __next__(self):
        if self.state == 0:
            loop = asyncio.get_event_loop()
            self.future = loop.create_future()
            loop.call_later(self.n, self.future.set_result, None)
            self.state = 1
        if self.state == 1:
            if not self.future.done():
                return next(iter(self.future))
            self.state = 2
        if self.state == 2:
            raise StopIteration
        raise AssertionError("invalid state")

Übersetzung nach C

Das Obige ist ziemlich viel Tipparbeit, aber es funktioniert und verwendet nur Konstrukte, die mit nativen Python/C-Funktionen definiert werden können.

Die beiden Klassen eigentlich ganz einfach in C zu übersetzen, geht aber über den Rahmen dieser Antwort hinaus.

1299110cookie-checkPython – wie man eine C-Funktion als Awaitable (Coroutine) implementiert

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy