Multithreading in Tkinter

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
UnixX
User
Beiträge: 4
Registriert: Donnerstag 6. Juni 2019, 15:47

Hallo Allerseits,

ich arbeite verzweifelt daran einen Multithread, genauer gesagt einen Dualthread unter Tkinter zu realisieren. Die beiden Threads sollen mit unterschiedlicher Taktung laufen, die durch die Funktion "sleep" definiert werden.
Das Problem ist, dass immer nur ein Thread lauft. Ziel wäre beide Threads mit unterschiedlicher Taktung gleichzeitig nebeneinander laufen zu lassen.
Dazu habe ich einen Beispielcode erstellt um zu verdeutlichen was ich genau meine.

#!/usr/bin/python

import Tkinter
import time
import threading
import random
import Queue
from threading import Thread


class GuiPart:
global th3
def __init__(self, master, queue, endCommand):
self.queue = queue
self.root=master

self.button1 = Tkinter.Button(master, text='Done', command=endCommand)
self.button1.place(x = 160, y = 50, width=80, height=20)

self.label1 = Tkinter.Label(self.root, text='label1' ,bg="white", fg="red",anchor = Tkinter.W)
self.label1.place(x = 60, y = 100, width=250, height=20)

self.label2 = Tkinter.Label(self.root, text='label2' ,bg="white", fg="red",anchor = Tkinter.W)
self.label2.place(x = 60, y = 150, width=250, height=20)

self.root.geometry("%dx%d+0+0" % (400, 400))

def processIncoming(self):

while self.queue.qsize():
try:
msg = self.queue.get(0)
print msg
except Queue.Empty:
pass

class ThreadedClient:

def __init__(self, master):

self.master = master

self.queue = Queue.Queue()

self.gui1 = GuiPart(master, self.queue, self.endApplication)

self.running = 1
self.thread1 = threading.Thread(target=self.workerThread1)
self.thread1.start()

self.periodicCall()

def periodicCall(self):

self.gui1.processIncoming()
if not self.running:

import sys
sys.exit(1)
self.master.after(100, self.periodicCall)

def workerThread1(self):

while self.running:

time.sleep(1)
msg = rand.random()
#self.queue.put(msg)
self.gui1.label1.config(text=msg)

def endApplication(self):
self.running = 0


class ThreadedClient2:

def __init__(self, master):

self.master = master

self.queue = Queue.Queue()

self.gui2 = GuiPart(master, self.queue, self.endApplication)

self.running = 1
self.thread2 = threading.Thread(target=self.workerThread2)
self.thread2.start()

self.periodicCall()

def periodicCall(self):

self.gui2.processIncoming()
if not self.running:

import sys
sys.exit(1)
self.master.after(100, self.periodicCall)

def workerThread2(self):

while self.running:

time.sleep(0.5)
msg = rand.random()
#self.queue.put(msg)
self.gui2.label2.config(text=msg)

def endApplication(self):
self.running = 0



rand = random.Random()

root = Tkinter.Tk()

client = ThreadedClient(root)
client2 = ThreadedClient2(root)

root.mainloop()

Ich hoffe, dass mir Jemand helfen kann.

Vielen Dank im voraus!
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Bitte benutz die Code-Tags, so ist dein Quellcode nicht lesbar. Im vollstaendigen Editor der "</>"-Button.

Und dann erlaube ich mir die Frage: warum reicht after nicht? Was sollen die Threads machen?
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

@UnixX: neue Programme sollte man nicht mehr mit Python2 schreiben. Python3.7 ist aktuell.
`global` sollte man nicht benutzen, an der Stelle ist es auch sinnlos und `th3` wird auch nirgends verwendet.
Warum erzeugst Du zweimal `GuiPart`-Instanzen für beide Threads, die Knöpfe und Labels exakt übereinander malen?
Für boolsche Werte gibt es True und False, das sollte man statt 1 oder 0 verwenden.
Mitten in `periodicCall` sollte kein sys.exit vorkommen. Gerade GUI-Programme wollen vielleicht noch aufräumen und nicht hart abgebrochen werden. Warum rufst Du periodicCall in Deiner Threadklasse per after immer wieder auf, statt das in der GUI mit processIncoming zu machen?

Aus einem Thread heraus, wie in `workerThread1` darf die GUI nicht verändert werden. Dafür hast Du doch die Queues und dieses komische periodicCall eingebaut.
UnixX
User
Beiträge: 4
Registriert: Donnerstag 6. Juni 2019, 15:47

Sirius3 hat geschrieben: Donnerstag 6. Juni 2019, 17:15 @UnixX: neue Programme sollte man nicht mehr mit Python2 schreiben. Python3.7 ist aktuell.
`global` sollte man nicht benutzen, an der Stelle ist es auch sinnlos und `th3` wird auch nirgends verwendet.
Warum erzeugst Du zweimal `GuiPart`-Instanzen für beide Threads, die Knöpfe und Labels exakt übereinander malen?
Für boolsche Werte gibt es True und False, das sollte man statt 1 oder 0 verwenden.
Mitten in `periodicCall` sollte kein sys.exit vorkommen. Gerade GUI-Programme wollen vielleicht noch aufräumen und nicht hart abgebrochen werden. Warum rufst Du periodicCall in Deiner Threadklasse per after immer wieder auf, statt das in der GUI mit processIncoming zu machen?

Aus einem Thread heraus, wie in `workerThread1` darf die GUI nicht verändert werden. Dafür hast Du doch die Queues und dieses komische periodicCall eingebaut.
Ich habe nicht viel Ahnung von python, oder programmieren allgemein :oops: möchte es aber lernen.
Wäre nett, wenn du den Code mal richtigstellen würdest.
UnixX
User
Beiträge: 4
Registriert: Donnerstag 6. Juni 2019, 15:47

__deets__ hat geschrieben: Donnerstag 6. Juni 2019, 17:06 Bitte benutz die Code-Tags, so ist dein Quellcode nicht lesbar. Im vollstaendigen Editor der "</>"-Button.

Und dann erlaube ich mir die Frage: warum reicht after nicht? Was sollen die Threads machen?
Der eine Thread aktualisiert die Bilder eines Videostreams, der andere überträgt die Mausposition und die Signale gedrückter Tasten an einen Roboter.
Wo genau müsste der Befehl "after" sinngemäß im Code stehen?
Benutzeravatar
__blackjack__
User
Beiträge: 14044
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@UnixX: Man sollte keine Programme mehr in Python 2 anfangen. Da läuft der Support am Ende des Jahres endgültig aus.

Auf Modulebene gehört nur Code der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Das bedeutet dann auch, das der Code nicht mehr einfach so auf die globale Variable `rand` zugreifen kann. Funktionen und Methoden sollten alles was sie ausser Konstanten benötigen als Argument(e) übergeben bekommen.

`place()` sollte man nicht benutzen weil das nur auf dem Rechner auf dem man das programmiert hat, oder solchen mit ähnlicher Anzeigeauflösung und Einstellungen funktioniert. Die Fenstergrösse sollte man nicht hart vorgeben. Muss man ja auch nur weil `place()` verwendet wurde. Die Stelle an der die Grösse des Hauptfensters festgelegt wird ist auch kaputt. Das passiert beim erstellen von jedem `GuiPart`-Objekt und immer mit der gleichen Grösse – egal wie viele man davon erstellen würde.

``global`` hat in einem sauberen Programm nichts zu suchen. Das hat an der Stelle wo es steht auch gar nicht die gewünschte Wirkung – denke ich – hab das noch nie auf Klassenebene gesehen. Der Name wird ja sowieso nirgends verwendet.

Der ``from threading import Thread``-Import wird nirgends verwendet.

Namen schreibt man in Python klein_mit_unterstrichen. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase).

`processIncoming()` verwendet unnötigerweise `qsize()`. Da würde man einfach solange ein nicht-blockierendes `get()` aufrufen bis man `Queue.Empty` auslöst und dann die ”Endlosschleife” abbrechen. Die 0 bei `get()` ist irreführend weil an der Stelle eine Wahrheitswert erwartet wird und wenn man weiss das die Methode ein Flag und einen Timeout-Wert als optionale Argumente erwartet, könnte man irrtümlich denken die 0 wäre ein Timeout-Wert. Da sollte `False` stehen, oder man ruft `get_nowait()` auf, was für den Leser deutlicher ist, wenn er keine Ahnung hat was das `False` bei `get()` bedeutet.

`ThreadedClient` und `ThreadedClient2` enthalten fast den gleichen Code. Das die Namen nummeriert sind, ist schon ein Hinweis, dass da etwas nicht stimmt. Auch an anderen Stellen werden Namen durchnummeriert – nicht machen! Entweder passende Namen ausdenken, oder vielleicht auch eine Datenstruktur wenn das Sinn macht, und natürlich auch überlegen ob man die Namen überhaupt braucht, beziehungsweise wie lange. Man muss nicht alles an Objekte binden, wenn man es danach nie wieder benötigt.

Beim `running`-Attribut gilt auch wieder: das sollte nicht 0 oder 1 sein – Python hat `False` und `True` für Wahrheitswerte.

`sys.exit(1)` sollte man nicht dort aufrufen wo es aufgerufen wird. Bei einem GUI-Programm sollte das beenden so gestaltet sein, dass Code nach dem `mainloop()`-Aufruf noch ausgeführt würde. Sonst hat man den Programmfluss nicht sauber unter Kontrolle.

Importe gehören an den Anfang des Moduls, nicht tief in Methoden versteckt und dort auch noch in einem ``if``-Zweig.

Deine Threads verändern Labeltexte – das ist ja gerade das was man verhindern will/muss und weshalb es die periodisch abgefragte(n) Queue(s) gibt.

Ich denke das ist noch zwei Nummern zu gross und Du solltest erst einmal die Grundlagen lernen.
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

Das ganze System ist ziemlich unsinnig, die vielen Klassen unnötig, das aktualisieren viel zu verquer. Beispiele, wie man mit Threads und Tkinter arbeitet, gibt es hier im Forum genug.

Hier auf das nötigste reduziert (für Python3):

Code: Alles auswählen

import tkinter as tk
import time
import random
from queue import Queue, Empty
from threading import Thread

class Gui:
    def __init__(self, master, queue):
        self.queue = queue
        self.root = master
        tk.Button(master, text='Done', command=master.quit).pack()
        self.labels = [
            tk.Label(self.root),
            tk.Label(self.root),
        ]
        for label in self.labels:
            label.pack()
        self.process_incoming()

    def process_incoming(self):
        try:
            while True:
                label, msg = self.queue.get_nowait()
                self.labels[label]['text'] = f"{msg:.2f}"
        except Empty:
            pass
        self.root.after(100, self.process_incoming)

def thread1(queue):
    while True:
        time.sleep(1)
        msg = random.random()
        queue.put((0, msg))

def thread2(queue):
    while True:
        time.sleep(1)
        msg = 2 * random.random()
        queue.put((1, msg))

def main():
    queue = Queue()
    Thread(target=thread1, args=(queue,), daemon=True).start()
    Thread(target=thread2, args=(queue,), daemon=True).start()
    root = tk.Tk()
    gui = Gui(root, queue)
    root.mainloop()

if __name__ == '__main__':
    main()
UnixX
User
Beiträge: 4
Registriert: Donnerstag 6. Juni 2019, 15:47

Sirius3 hat geschrieben: Donnerstag 6. Juni 2019, 17:47 Das ganze System ist ziemlich unsinnig, die vielen Klassen unnötig, das aktualisieren viel zu verquer. Beispiele, wie man mit Threads und Tkinter arbeitet, gibt es hier im Forum genug.

Hier auf das nötigste reduziert (für Python3):

Code: Alles auswählen

import tkinter as tk
import time
import random
from queue import Queue, Empty
from threading import Thread

class Gui:
    def __init__(self, master, queue):
        self.queue = queue
        self.root = master
        tk.Button(master, text='Done', command=master.quit).pack()
        self.labels = [
            tk.Label(self.root),
            tk.Label(self.root),
        ]
        for label in self.labels:
            label.pack()
        self.process_incoming()

    def process_incoming(self):
        try:
            while True:
                label, msg = self.queue.get_nowait()
                self.labels[label]['text'] = f"{msg:.2f}"
        except Empty:
            pass
        self.root.after(100, self.process_incoming)

def thread1(queue):
    while True:
        time.sleep(1)
        msg = random.random()
        queue.put((0, msg))

def thread2(queue):
    while True:
        time.sleep(1)
        msg = 2 * random.random()
        queue.put((1, msg))

def main():
    queue = Queue()
    Thread(target=thread1, args=(queue,), daemon=True).start()
    Thread(target=thread2, args=(queue,), daemon=True).start()
    root = tk.Tk()
    gui = Gui(root, queue)
    root.mainloop()

if __name__ == '__main__':
    main()

Das werde ich mir mal zu Gemüte ziehen.

Vielen Dank für deine Antwort!
Antworten