Threading in Python

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Soxxes
User
Beiträge: 12
Registriert: Mittwoch 28. August 2019, 14:53

Hallo zusammen,

ich bastle gerade an einem Programm, in welchem ich threading nutze.
Da ich das zum ersten Mal nutze, bin ich mir bei einigen Sachen noch sehr unsicher.
Das Ganze ist innerhalb einer GUI, die aber für mein Problem bzw. meine Fragen nicht wichtig ist. Deshalb poste ich es auch nicht ins GUI-Forum.

Ich habe die Programm-Teile rausgesucht, weil da wirklich viel dabei ist, was für meine Fragen irrelevant ist.

1. Klassendefinition mit Konstruktor:

Code: Alles auswählen

class Hauptfenster(tk.Frame):
    def __init__(self, parent):
        self.parent = parent
        self.abbruch_event = threading.Event()
        self.thread_event = threading.Event()
        self.Initialize_user_interface()

2. Starten einer Funktion namens rechteck_kurs:

Code: Alles auswählen

def Start(self):
        # sicherstellen, dass bei mehrmaligem Start-Drücken
        # nicht mehrere Durchläufe/Threads parallel gestartet werden
        if threading.active_count() < 2:
            self.thread_event.set()
            self.thread_kurs = threading.Thread(target=vk.rechteck_kurs, args=(self.thread_event, self.abbruch_event, ))
            # falls man einen Neustart machen möchte, ist das notwendig,
            # da sonst abbruch_event konstant auf True ist
            self.abbruch_event.clear()
            self.thread_kurs.start()
(vk.rechteck_kurs deshalb, weil die Funktion noch in einer anderen .py-Datei definiert ist.)


3. rechteck_kurs-Funktion:

Code: Alles auswählen

# ----- Rechteck-Kurs -----
def rechteck_kurs(event, abbruch):
    while event.is_set():
        # Start bei (x,y,z) = (0,0,0)
        x = 0
        y = 0
        while y < 16:
            y += 1
            if not event.is_set():
                # ... macht irgenwas (unwichtig für threading)
                event.wait()
            if abbruch.is_set():
                exit()
            time.sleep(1)
            # es folgen noch weitere while-Schleifen, die aber ähnlich bzw. genauso aufgebaut sind
(kurz zu Erklärung: dieses Programm soll nach dem Drücken eines Buttons ablaufen. Da ich eine while-Schleife habe, würde das Hauptfenster des GUI blockieren, bis das Programm abgearbeitet ist. Aus diesem Grund dachte ich mir, den Durchlauf des rechteck_kurs-Programms in einen neuen Thread "auszulagern".)


4. Beenden des Threads:

Code: Alles auswählen

def Abbruch(self):
        if not self.thread_event.is_set():
            mb.showinfo("E-Stop aktiv", "Ein E-Stop muss erst aufgehoben werden.")
        else:
            self.abbruch_event.set()
            # Selbsttest 1
            print(threading.active_count())
            print(threading.enumerate())
            self.thread_kurs.join()
            # Selbsttest 2
            print(threading.active_count())
            print(threading.enumerate())

Nun zu meiner Frage:
Bevor ich "Start" drücke, bekomme ich für

Code: Alles auswählen

print(threading.active_count())
print(threading.enumerate())
1
[<_MainThread(MainThread, started 7164)>]


Bei Selbsttest 1 bekomme ich dann:
3
[<_MainThread(MainThread, started 7408)>, <Thread(Thread-1, started daemon 5856)>, <Thread(Thread-2, started 8144)>]


Bei Selbsttest 2 bekomme ich:
2
[<_MainThread(MainThread, started 7408)>, <Thread(Thread-1, started daemon 5856)>]



Wenn ich dann das Hauptfenster schließe, bekomme ich ganz normal:
[Finished in 121.468s]
(Ich nutze Atom.)

Nun zu meinen Fragen:
Offenbar wird der Thread thread_kurs nach diesem .join() beendet. Ist das so überhaupt Best Practice zum Beenden eines Threads?
Was ist dieses daemon? Ich habe in der Dokumentation nachgelesen:
https://docs.python.org/3/library/threa ... ead.daemon
Aber habe das nicht wirklich verstanden.
Und wieso ist dieses daemon noch da, wenn ich den Thread eigentlich beendet habe und nur noch der MainThread da sein sollte?
Ist das schlimm?
Wenn ich meinen Code so lese, habe ich nicht das Gefühl, dass das alles so richtig schön geschrieben ist und ich bin auch noch am optimieren, aber bisher klappt alles, was klappen soll. Mein Anspruch ist aber, dass es "schön" geschrieben ist und funktioniert. Über Anmerkungen diesbezüglich freue ich mich auch. :) Ich lerne gerne dazu.



Danke an die, die sich Zeit genommen haben, das alles zu lesen.
Über Hilfe würde ich mich sehr freuen. :)



Liebe Grüße,
Marc
Benutzeravatar
__blackjack__
User
Beiträge: 14047
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Soxxes: Du solltest nicht die Thread-Anzahl verwenden um zu prüfen ob ein *bestimmter* Thread schon läuft oder nicht. Denn an der Anzahl kann man das ja gar nicht erkennen, beziehungsweise nur wenn es tatsächlich nur einen Thread neben dem Hauptthread geben kann. Die Bedingung wäre also eher:

Code: Alles auswählen

    def start(self)
        if self.thread_kurs and not self.thread_kurs.is_alive():
          ...
Und bei Dir laufen ja drei Threads und ich sehe am gezeigten Code nicht wo der dritte her kommt.

Der Name `thread_kurs` ist irgendwie yodamässig. Das ist ein Kurs über Threads. Wenn man einen Thread benennen will wäre das eher `kurs_thread`.

In der `rechteck_kurs()`-Funktion das `exit()` ist falsch. Das sollte wohl eher einfach ein ``return`` sein um die Funktion zu beenden. (Ich gehe davon aus, dass wir von `sys.exit()` sprechen.)

Das beenden ist so schon okay, wobei der Code in dem Thread so geschrieben sein sollte, dass er möglichst zeitnah auf das `abbruch_event` reagiert, denn `join()` blockiert die GUI solange.

Das Daemon-Flag bei einem Thread sorgt dafür dass der (hart) beendet wird wenn der Hauptthread endet. Wenn man das nicht setzt, dann endet das Programm erst wenn alle nicht-daemon-Threads von selbst am Ende sind.

Und wirf mal einen Blick in den Style Guide for Python Code was die Namensschreibweisen angeht.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Soxxes
User
Beiträge: 12
Registriert: Mittwoch 28. August 2019, 14:53

Hi blackjack,

danke für deine Antwort. :)

Tatsächlich starte ich nur einen Thread im gesamten Code. Das sind alle Stellen, wo irgendwas mit Threading vorkommt.
An einer Stelle gibt es noch

Code: Alles auswählen

self.thread_event.clear()
und

Code: Alles auswählen

self.thread_event.set()
um das

Code: Alles auswählen

if not event.is_set():
                # ... macht irgenwas (unwichtig für threading)
                event.wait()
zu triggern, aber das dürfte eigentlich keinen neuen Thread starten, oder?
Und bei Dir laufen ja drei Threads und ich sehe am gezeigten Code nicht wo der dritte her kommt.
Ja, das verstehe ich nämlich auch nicht.


Ich habe mal folgendes probiert:

Code: Alles auswählen

def Start(self):
        # sicherstellen, dass bei mehrmaligem Start-Drücken
        # nicht mehrere Durchläufe/Threads parallel gestartet werden
        if threading.active_count() < 2:
            print("Vor thread.set():", threading.active_count())
            print(threading.enumerate())
            self.thread_event.set()
            print("Vor Threadbenennung:", threading.active_count())
            print(threading.enumerate())
            self.thread_kurs = threading.Thread(target=vk.rechteck_kurs, args=(self.thread_event, self.abbruch_event, ))
            # falls man einen Neustart machen möchte, ist das notwendig,
            # da sonst abbruch_event konstant auf True ist
            self.abbruch_event.clear()
            print("Direkt vor Start:", threading.active_count())
            print(threading.enumerate())
            self.thread_kurs.start()
            print("Nach Start:", threading.active_count())
            print(threading.enumerate())
Und das ergibt folgende Ausgabe:
Vor thread.set(): 1
[<_MainThread(MainThread, started 11040)>]
Vor Threadbenennung: 2
[<_MainThread(MainThread, started 11040)>, <Thread(Thread-1, started daemon 13428)>]
Direkt vor Start: 2
[<_MainThread(MainThread, started 11040)>, <Thread(Thread-1, started daemon 13428)>]
Nach Start: 3
[<_MainThread(MainThread, started 11040)>, <Thread(Thread-1, started daemon 13428)>, <Thread(Thread-2, started 12772)>]


Keine Ahnung wieso, aber durch das

Code: Alles auswählen

self.thread_event.set()
scheint dieser komische Thread erstellt zu werden.


Bei den Bezeichnungen hast du völlig Recht, da muss ich auf jeden Fall nochmal drüber schauen.
In der `rechteck_kurs()`-Funktion das `exit()` ist falsch. Das sollte wohl eher einfach ein ``return`` sein um die Funktion zu beenden. (Ich gehe davon aus, dass wir von `sys.exit()` sprechen.)
Ich hatte da zuerst ein

Code: Alles auswählen

break()
drin, aber das hat nicht so wirklich funktioniert. Mit dem exit() klappt es irgendwie.
Ist denn

Code: Alles auswählen

exit()
<=> äquivalent zu

Code: Alles auswählen

sys.exit()
?

Ich habe keinen Namespace oder sonst etwas von sys verwendet, falls du das meinst.

denn `join()` blockiert die GUI solange.
Das habe ich auch schon gemerkt. :D Für einen kurzen Augenblick sieht es so aus, als sei der Abbruch-Button weiterhin gedrückt. Da muss ich also auch nochmal schauen.

Das Daemon-Flag bei einem Thread sorgt dafür dass der (hart) beendet wird wenn der Hauptthread endet. Wenn man das nicht setzt, dann endet das Programm erst wenn alle nicht-daemon-Threads von selbst am Ende sind.
Das habe ich ja eigentlich nicht gemacht, also ich setze nirgends so ein Daemon-Flag. Sollte ich das denn tun? Und wenn ja, an welcher Stelle?


Liebe Grüße,
Marc
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

Genau deshalb solltest Du ja active_count nicht benutzen; welche Bibliothek aus welchen Gründen auch immer Threads startet, ist deren interne Angelegenheit. Ja `exit` ist das `sys.exit`. Und `break` (ohne Klammern) funktioniert nicht, weil Du mehrere Schleifen ineinander geschachtelt hast. `return` ist das Mittel der Wahl.

Ich blicke durch Deine vielen Events nicht durch. Warum wird `event` sowohl in der äußersten while-Schleife als auch irgendwo tief verschachtelt benutzt? `event` ist auch ein zu generischer Name, dass man keine Chance hat, das zu verstehen.
Soxxes
User
Beiträge: 12
Registriert: Mittwoch 28. August 2019, 14:53

Hi Sirius3,

dann werde ich das mal schnell ändern. :)
Allerdings ... ich bin mir nicht sicher, ob das klappt:

Code: Alles auswählen

if self.thread_kurs and not self.thread_kurs.is_alive():
weil thread_kurs existiert ja zum Zeitpunkt vor dem ersten Start nicht.

Ahhh ... Danke dir. Die äußere while-Schleife ist völlig redundant, da ich rechteck_kurs über das abbruch_event schließe.
Das mit dem Abbruch habe ich nachträglich hinzugefügt. Die Schleife kann ich also rausnehmen.

Ich merke schon, ich bin echt nicht gut darin, vernünftige Bezeichnungen zu finden. ^^

Schau mal was ich hier gefunden habe:
https://www.bogotobogo.com/python/Multi ... hreads.php
Da wird gesagt, dass daemon threads automatisch geschlossen werden, wenn das Hauptprogramm beendet wird. Oder verstehe ich da etwas falsch?


Liebe Grüße,
Marc
Soxxes
User
Beiträge: 12
Registriert: Mittwoch 28. August 2019, 14:53

Ich hab's herausgefunden.
Ich hatte aus der start-Methode etwas rausgelöscht, weil ich dachte, dass es für euch nur noch mehr Zeug zum Lesen wäre, es aber nichts mit der Sache zu tun hätte. Falsch.

Code: Alles auswählen

def Start(self):
        # sicherstellen, dass bei mehrmaligem Start-Drücken
        # nicht mehrere Durchläufe/Threads parallel gestartet werden
        if threading.active_count() < 2:
        # klappt noch nicht
        #if self.thread_kurs and not self.thread_kurs.is_alive():
            print("Vor thread.set():", threading.active_count())
            print(threading.enumerate())
            self.thread_event.set()
            client.connect(broker_address)
            client.loop_start()
            print("Vor Threadbenennung:", threading.active_count())
            print(threading.enumerate())
            self.thread_kurs = threading.Thread(target=vk.rechteck_kurs, args=(self.thread_event, self.abbruch_event, ))
            # falls man einen Neustart machen möchte, ist das notwendig,
            # da sonst abbruch_event konstant auf True ist
            self.abbruch_event.clear()
            print("Direkt vor Start:", threading.active_count())
            print(threading.enumerate())
            self.thread_kurs.start()
            print("Nach Start:", threading.active_count())
            print(threading.enumerate())
Durch die Verbindung mit einem MQTT-Broker durch

Code: Alles auswählen

client.connect(broker_address)
client.loop_start()
wird offenbar ein daemon thread erzeugt.

Meine Frage wäre jetzt nur noch, wie ich das richtig mit dem

Code: Alles auswählen

if self.thread_kurs and not self.thread_kurs.is_alive():
einbaue.

Und vielleicht kurz für's Verständnis:
Was ist denn der Unterschied zwischen exit() und return? Und wieso sollte ich nicht exit() nutzen? Funktioniert hat es nämlich. :D


Liebe Grüße,
Marc


edit:

Code: Alles auswählen

if abbruch.is_set():
	return
habe ich jetzt direkt unter die while-Schleifen geschrieben, so dass es das erste ist, was abgefragt wird.
Die oberste while-Schleife habe ich rausgenommen.
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

Durch die vielen Code-Fetzen ist es schwierig zu verstehen, was jetzt wo steht und warum dann was nicht so funktioniert wie es soll.

Die if-Abfrage muß so heißen:

Code: Alles auswählen

def start(self, vk, broker_address):
        # sicherstellen, dass bei mehrmaligem Start-Drücken
        # nicht mehrere Durchläufe/Threads parallel gestartet werden
        if not self.mqtt_connected:
            client.connect(broker_address)
            client.loop_start()
            self.mqtt_connected = True
        if self.thread_kurs is None or not self.thread_kurs.is_alive():
            self.thread_event.set()
            self.abbruch_event.clear()
            self.thread_kurs = threading.Thread(target=vk.rechteck_kurs, args=(self.thread_event, self.abbruch_event, ))
            self.thread_kurs.start()
Der MQTT-Broker sollte nach meinem Gefühl gar nicht darin stehen. Woher kommt denn `broker_address`?
Woher kommt denn `vk`? Ich hab das mal als Parameter mit übergeben.
Soxxes
User
Beiträge: 12
Registriert: Mittwoch 28. August 2019, 14:53

Wenn ich es so mache, wie du sagtest:

Code: Alles auswählen

def Start(self):
        if not self.mqtt_connected:
            Klassen.client.connect(Klassen.broker_address)
            Klassen.client.loop_start()
            self.mqtt_connected = True
        # sicherstellen, dass bei mehrmaligem Start-Drücken
        # nicht mehrere Durchläufe/Threads parallel gestartet werden
        if self.thread_kurs is None or not self.thread_kurs.is_alive():
            self.thread_event.set()
            self.thread_kurs = threading.Thread(target=vk.rechteck_kurs, args=(self.thread_event, self.abbruch_event, ))
            # falls man einen Neustart machen möchte, ist das notwendig,
            # da sonst abbruch_event konstant auf True ist
            self.abbruch_event.clear()
            self.thread_kurs.start()
bekomme ich wieder die Fehlermeldung:
[...]line 88, in Start
if self.thread_kurs is None or not self.thread_kurs.is_alive():
AttributeError: 'Hauptfenster' object has no attribute 'thread_kurs'


Aber das kann doch auch eigentlich nicht gehen, weil thread_kurs erst nach der if-Abfrage erstellt wird.
Für mqtt_connected hab ich im Konstruktor jetzt einfach gesagt:

Code: Alles auswählen

class Hauptfenster(tk.Frame):
    def __init__(self, parent):
        self.parent = parent
        self.abbruch_event = threading.Event()
        self.thread_event = threading.Event()
        self.mqtt_connected = False
        self.Initialize_user_interface()
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

Und genau das ist Dein Fehler: alle Attribute müssen in __init__ schon angelegt werden, und sei es mit dem Wert None.
Soxxes
User
Beiträge: 12
Registriert: Mittwoch 28. August 2019, 14:53

Wirklich alle?
Das wusste ich nicht. Danke für die Aufklärung. :)

Was ist nun der Unterschied zwischen exit() und return?
Benutzeravatar
__blackjack__
User
Beiträge: 14047
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Soxxes: Nur `exit()` gibt es eigentlich nicht, das ist nicht dokumentiert. Aber CPython hat das `sys.exit()` immer im globalen Namensraum. Und das bricht das gesamte Programm ab — wenn es im Hauptthread aufgerufen wird. In anderen Threads führt es zu einer Ausnahme die dann als Nebenwirkung nur diesen Thread abbricht.

Und ``return`` solltest Du ja eigentlich kennen — das beendet die Funktion in der es ausgeführt wird.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Antworten