Bennis Run – Threads im mainloop()

Wir zeigen anhand Benjamins schönem Beispielprogramm, wie Funktionen als eigenständige Threads ausgeführt werden können.

Es verwendet das simulierte RPi.GPIO.add_event_callback(…) von RevPiModIO – Die Funktion ist bekannt vom RaspberryPi.

In „BennisRun“ wird durch setzen des Inputs eine Funktion gestartet, die nacheinander alle Outputs auf True setzt. Sie verwendet die sleep() Funktion und braucht daher „viel“ Zeit. Wenn der Input zurückgesetzt wird, startet eine andere Funktion, die alle Outputs nacheinander auf False setzt.

Normalerweise werden alle, durch Events ausgelösten, Funktionen komplett abgearbeitet, bevor der mainloop() weiter läuft und weitere Events ausgelöst werden können. Dies ist gewollt und wird realisiert, indem die Funktionen vom selben Thread ausgeführt werden, den auch der mainloop() verwendet (Die Prozessbildaktualisierung läuft in einem anderen Thread und wird dadurch natürlich NICHT angehalten! Es sei denn, beim Eintritt in den mainloop() wird der parameter freeze=True übergeben).

Für „BennisRun“ bedeutet dies, dass erst die eine Funktion alle Outputs auf True setzen muss und erst dann die andere Funktion aufgerufen werden kann, die alle Outputs wieder auf False setzt.

ES SEI DENN: Wir verwenden bei reg_event() den Parameter „as_thread=True“! Dies ähnelt dem Verhalten von add_event_callback(…).

Alte RevPiModIO Syntax

self.revpi.devices["devname"].reg_event("Inputname", self.eventfunktion, edge=revpimodio.RISING, as_thread=True)

Neue RevPiModIO2 Syntax

self.revpi.io.Inputname.reg_event(self.eventfunktion, edge=revpimodio2.RISING, as_thread=True)

Dann werden die Funktionen als einzelne Threads gestartet und können parallel laufen – Wenn im Code nicht überprüft, sogar die selbe Funktion mehrfach (Die Funktion muss als Übergabeparameter das Keyword „thread“ enthalten).

Das sieht dann so aus:

Und das ist der kleine Quelltext:

Quelltext für RevPiModIO Version 2

#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# (c) Benjamin
"""Beispielprogramm fuer mainloop() und as_thread."""
import revpimodio2
import time

# Diese Werte müssen an die eigene piCtory Konfiguration angepasst werden!
# Name des Inputs: I_1
outputformat = "O_{}"


class RevPiModIOTest():

    """Programmklasse."""

    def __init__(self):
        """Instanziiert unser Programm."""
        self.out = [outputformat.format(x) for x in range(1, 15)]

        # RevPiModIO2 Instanziieren und "Programm beenden"-Signal verwalten
        self.revpi = revpimodio2.RevPiModIO(autorefresh=True)
        self.revpi.handlesignalend(self.exitfunktion)

        # Events Registrieren, die als THREAD ausgeführt werden
        self.revpi.io.I_1.reg_event(
            self.eventfunktion1, edge=revpimodio2.RISING, as_thread=True
        )
        self.revpi.io.I_1.reg_event(
            self.eventfunktion2, edge=revpimodio2.FALLING, as_thread=True
        )

    def exitfunktion(self):
        """Diese Funktion wird beim Beenden des Programms ausgefuert."""
        self.revpi.core.A1 = revpimodio2.OFF

    def eventfunktion1(self, thread):
        """Lichter der Reihe nach einschalten."""
        for i in range(len(self.out)):
            self.revpi.io[self.out[i]].value = True
            time.sleep(0.1)

    def eventfunktion2(self, thread):
        """Lichter der Reihe nach ausschalten."""
        for i in range(len(self.out)):
            self.revpi.io[self.out[i]].value = False
            time.sleep(0.1)

    def start(self):
        """Startet den mainloop()."""
        self.revpi.core.A1 = revpimodio2.GREEN
        print("Gehe in den mainloop()")
        self.revpi.mainloop()

        print("Verlasse mainloop()")


if __name__ == "__main__":
    root = RevPiModIOTest()
    root.start()
Quelltext für RevPiModIO Version 1
#!/usr/bin/python3
# (c) Benjamin
# -*- coding: utf-8 -*-
import revpimodio
import signal
import time

# Diese Werte müssen an die eigene piCtory Konfiguration angepasst werden!
outputformat = "O_{}"
iodevname = "dio02"
eventinput = "I_1"


class RevPiModIOTest():

    def __init__(self):
        self.out = [outputformat.format(x) for x in range(1, 15)]
        self.revpi = revpimodio.RevPiModIO(auto_refresh=True)

        # Events Registrieren, die als THREAD ausgeführt werden
        self.revpi.devices[iodevname].reg_event(
            eventinput, self.eventfunktion1,
            edge=revpimodio.RISING, as_thread=True
        )
        self.revpi.devices[iodevname].reg_event(
            eventinput, self.eventfunktion2,
            edge=revpimodio.FALLING, as_thread=True
        )

        signal.signal(signal.SIGINT, self._sigexit)
        signal.signal(signal.SIGTERM, self._sigexit)

    def _sigexit(self, signum, frame):
        self.revpi.devices.exit()
        print("Verlasse mainloop()")

    def eventfunktion1(self, thread):
        for i in range(len(self.out)):
            self.revpi.devices[iodevname][self.out[i]].value = 1
            time.sleep(0.1)

    def eventfunktion2(self, thread):
        for i in range(len(self.out)):
            self.revpi.devices[iodevname][self.out[i]].value = 0
            time.sleep(0.1)

    def start(self):
        self.revpi.devices.core.A1 = revpimodio.GREEN
        print("Gehe in den mainloop()")
        self.revpi.devices.mainloop()
        self.revpi.devices.core.A1 = revpimodio.OFF
        self.revpi.devices.writeprocimg()


if __name__ == "__main__":
    root = RevPiModIOTest()
    root.start()
Die vollen Möglichkeiten!

Wir haben aber noch weitaus mehr Möglichkeiten, die in unserem übergebenen „thread“ Objekt, vom Typen RevPiCallback, stecken!

In diesem extremeren Beispiel verwenden wir nur eine einzige Event-Funktion def runner(self, thread):. Zwei Inputs „I_1“, „I_2“ für die Lauflichtrichtung und „I_3“ als Abbruchbedingung um frühzeitig das Verlassen der for-Schleifen zu ermöglichen.

In der „runner“ Funktion fragen wir über thread.ioname, den Namen des Inputs ab, der die Funktion aktuell aufgerufen hat. Damit bestimmen wir die Laufrichtung.

Über thread.iovalue erhalten wir den zum Auslösezeitpunkt anstehenden Wert des Inputs. Mit dessen Zuweisung an die Outputs schalten wir diese an oder aus.

Zum Warten verwenden wir nun kein time.sleep(0.1) mehr, da wir dort einfach feststecken würden und warten MÜSSEN. Unser „thread“ Objekt stellt uns das Event thread.exit zur Verfügung, mit dessen .wait(sekunden) Funktion wir warten. Dieses Warten kann durch .set() jederzeit abgebrochen werden.

Und genau dieses .set() ruft unsere Funktion def killer(self, ioname=None, iovalue=None): für jeden aktiven Thread auf, welche durch „I_3“ als normales Event gestartet wird!

Mit einem globalen threading.Lock() Objekt self.threadlock = Lock() verhindern wir den gleichzeitigen Zugriff auf die Liste mit den derzeit aktiven Threads self.lst_threads = [] (beachtet die Umfangreiche Quellcodedoku)

Quelltext für RevPiModIO Version 2

#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# (c) Benjamin, Sven Sager
"""Beispielprogramm fuer mainloop() und as_thread."""
import revpimodio2
from threading import Lock

# Diese Werte müssen an die eigene piCtory Konfiguration angepasst werden!
# Namen der Inputs: I_1, I_2, I_3
outputformat = "O_{}"


class RevPiModIOTest():

    """Programmklasse."""

    def __init__(self):
        """Instanziiert unser Programm."""
        self.out = [outputformat.format(x) for x in range(1, 15)]

        # RevPiModIO2 Instanziieren und "Programm beenden"-Signal verwalten
        self.revpi = revpimodio2.RevPiModIO(autorefresh=True)
        self.revpi.handlesignalend(self.exitfunktion)

        # Threadliste führen
        self.lst_threads = []
        self.threadlock = Lock()

        # Events Registrieren, die als THREAD ausgeführt werden
        self.revpi.io.I_1.reg_event(self.runner, as_thread=True)
        self.revpi.io.I_2.reg_event(self.runner, as_thread=True)

        # Event um alle aktiven Threads zu "killen" registrieren
        self.revpi.io.I_3.reg_event(self.killer, edge=revpimodio2.RISING)

    def exitfunktion(self):
        """Diese Funktion wird beim Beenden des Programms ausgefuert."""
        print("Beende alle laufenden Threads")
        self.killer()
        self.revpi.core.A1 = revpimodio2.OFF

    def killer(self, ioname=None, iovalue=None):
        """ Alle laufenden Threads beenden.

        Lockobjekt holen, welches verhindert, dass die abgebrochenen Threads
        sich selbst aus der Liste loeschen und diese beim iterieren veraendern
        Die abgebrochenen Threads benoetigen auch dieses Lock-Objekt und
        warten auf dessen Freigabe

        """
        with self.threadlock:

            # Durch alle aktiven Threads gehen und sie beenden
            for thread in self.lst_threads:

                # Die thread.stop() Funktion von RevPiCallback aufrufen,
                # welche das exit() Event setzt. Dieses Event verwenden
                # wir als Abbruchbedingung.
                thread.stop()

    def runner(self, thread):
        """Lauflicht starten."""
        # Diesen Thread in aktive Liste aufnehmen
        with self.threadlock:
            self.lst_threads.append(thread)

        # In thread.ioname steht der Name des Inputs, der das Event auslöste
        if thread.ioname == "I_1":
            for i in range(len(self.out)):

                # In thread.iovalue steht der Wert zum Auslösezeitpunkt
                self.revpi.io[self.out[i]].value = thread.iovalue

                # Über das Event thread.exit(sekunden) kann gewartet werden.
                # Wird das Event zum Abbrechen auf .set() gesetzt, erhält man
                # True und bricht die Schleife ab. Wenn die Zeit abgelaufen ist
                # ohne ein .set() zum Abbrechen erhält man ein False
                if thread.exit.wait(0.1):
                    break

                # Bei anderen Schleifenkonstruktionen kann das exit-Event
                # auch über thread.exit.is_set() abgefragt werden, welches
                # True zurückgibt, wenn für den Thread die .stop() Funktion
                # aufgerufen wurde.
                #
                # while not thread.exit.is_set():
                #    Bleibt in der Whileschleife, bis an anderer Stelle
                #    die .stop() Funktion des threads aufgerufen wird.

        else:
            # Alles wie oben, nur von rechts nach links
            for i in range(len(self.out) - 1, -1, -1):
                self.revpi.io[self.out[i]].value = thread.iovalue
                if thread.exit.wait(0.1):
                    break

        with self.threadlock:
            # Diesen Thread nach Fertigstellung aus aktive Liste entfernen
            self.lst_threads.remove(thread)

    def start(self):
        """Startet den mainloop()."""
        self.revpi.core.A1 = revpimodio2.GREEN
        print("Gehe in den mainloop()")
        self.revpi.mainloop()

        print("Verlasse mainloop()")


if __name__ == "__main__":
    root = RevPiModIOTest()
    root.start()

 

Quelltext für RevPiModIO Version 1
#!/usr/bin/python3
# (c) Benjamin, Sven Sager
# -*- coding: utf-8 -*-
import revpimodio
import signal
from threading import Lock

# Diese Werte müssen an die eigene piCtory Konfiguration angepasst werden!
outputformat = "O_{}"
iodevname = "dio02"
eventlinksrechts = "I_1"
eventrechtslinks = "I_2"
eventabbruch = "I_3"


class RevPiModIOTest():

    def __init__(self):
        self.out = [outputformat.format(x) for x in range(1, 15)]
        self.revpi = revpimodio.RevPiModIO(auto_refresh=True)

        # Threadliste führen
        self.lst_threads = []
        self.threadlock = Lock()

        # Events Registrieren, die als THREAD ausgeführt werden
        self.revpi.devices[iodevname].reg_event(
            eventlinksrechts, self.runner, as_thread=True
        )
        self.revpi.devices[iodevname].reg_event(
            eventrechtslinks, self.runner, as_thread=True
        )

        # Event um alle aktiven Threads zu "killen" registrieren
        self.revpi.devices[iodevname].reg_event(
            eventabbruch, self.killer, edge=revpimodio.RISING
        )

        signal.signal(signal.SIGINT, self._sigexit)
        signal.signal(signal.SIGTERM, self._sigexit)

    def _sigexit(self, signum, frame):
        print("Verlasse mainloop()")
        self.revpi.devices.exit()
        print("Beende alle laufenden Threads")
        self.killer()

    def killer(self, ioname=None, iovalue=None):
        # Lockobjekt holen, welches verhindert, dass die abgebrochenen Threads
        # sich selbst aus der Liste löschen und diese beim iterieren verändern
        # Die abgebrochenen Threads benötigen auch dieses Lock-Objekt und
        # warten auf dessen Freigabe
        with self.threadlock:

            # Durch alle aktiven Threads gehen und sie beenden
            for thread in self.lst_threads:

                # Die thread.stop() Funktion von RevPiCallback aufrufen,
                # welche das exit() Event setzt. Dieses Event verwenden
                # wir als Abbruchbedingung.
                thread.stop()

    def runner(self, thread):
        # Diesen Thread in aktive Liste aufnehmen
        with self.threadlock:
            self.lst_threads.append(thread)

        # In thread.ioname steht der Name des Inputs, der das Event auslöste
        if thread.ioname == eventlinksrechts:
            for i in range(len(self.out)):

                # In thread.iovalue steht der Wert zum Auslösezeitpunkt
                self.revpi.devices[iodevname][self.out[i]].value = thread.iovalue

                # Über das Event thread.exit(sekunden) kann gewartet werden.
                # Wird das Event zum Abbrechen auf .set() gesetzt, erhält man
                # True und bricht die Schleife ab. Wenn die Zeit abgelaufen ist
                # ohne ein .set() zum Abbrechen erhält man ein False
                if thread.exit.wait(0.1):
                    break
                
                # Bei anderen Schleifenkonstruktionen kann das exit-Event
                # auch über thread.exit.is_set() abgefragt werden, welches
                # True zurückgibt, wenn für den Thread die .stop() Funktion
                # aufgerufen wurde.
                #
                # while not thread.exit.is_set():
                #    Bleibt in der Whileschleife, bis an anderer Stelle
                #    die .stop() Funktion des threads aufgerufen wird.

        # Alles wie oben, nur von rechts nach links durch Abfrage auf anderen Input
        if thread.ioname == eventrechtslinks:
            for i in range(len(self.out) - 1, -1, -1):
                self.revpi.devices[iodevname][self.out[i]].value = thread.iovalue
                if thread.exit.wait(0.1):
                    break

        with self.threadlock:
            # Diesen Thread nach Fertigstellung aus aktive Liste entfernen
            self.lst_threads.remove(thread)

    def start(self):
        self.revpi.devices.core.A1 = revpimodio.GREEN
        print("Gehe in den mainloop()")
        self.revpi.devices.mainloop()
        self.revpi.devices.core.A1 = revpimodio.OFF
        self.revpi.devices.writeprocimg()


if __name__ == "__main__":
    root = RevPiModIOTest()
    root.start()