Skip to content

PyQt5 - QThread

Python3
  • Eines der Geheimnisse, was mich schon was länger beschäftigt ist, wie man parallele Prozesse mit Python verwaltet. Dazu findet man viele Beispiele und man rätselt und rätselt.....

    Mal gehen die Beispiele nicht, mal versteht man sie nicht. Dabei habe ich nur was gesucht, was mir möglichst einfach darstellt, wie man so was grundsätzlich aufbaut. Der Rest kommt dann mit der Zeit, wenn ich lange genug damit herumgespielt habe 😊

    Ok, warum das Ganze? Ich habe in meinem Restic UI Prozesse, die was länger dauern. Ich möchte dem Anwender das signalisieren, mittels einem waitingspinnerwidget. Ok, so weit hatte ich das auch hinbekommen, nur was mache ich mit einem Ergebnis in diesem Prozess? Das muss ja irgendwie zurück 🤔

    Ich habe dann diese Code hier gefunden und zum Laufen bekommen. YEAH

    #!/usr/bin/env python3
    
    from PyQt5.QtCore import QObject, QThread
    from PyQt5.QtCore import pyqtSlot, pyqtSignal
    from PyQt5.QtWidgets import QMainWindow
    
    import sys
    import time
    from PyQt5.QtWidgets import QApplication
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            print("Base init")
    
            self.thread = QThread()
            self.w = Worker()
            self.w.finished[str].connect(self.onFinished)
            self.w.moveToThread(self.thread)
            self.thread.started.connect(self.w.work)
            self.thread.start()
    
        @pyqtSlot(str)
        def onFinished(self, i):
            #print("Base caught finished, {}".format(i))
            print("Worker end", i)
    
    class Worker(QObject):
        finished = pyqtSignal(str)
    
        def __init__(self):
            print("Worker init")
            super().__init__()
    
        def work(self):
            print("Worker work")
            time.sleep(5)
            self.finished.emit('TEXT')
    
    if __name__ == "__main__":
    
        app = QApplication(sys.argv)
        w = MainWindow()
        w.show()
    
        sys.exit(app.exec_())
    

    Das ist jetzt nicht so verdammt schwer, ein wenig hat man ja schon gelernt. Es geht ein Fenster auf, der Hauptprozess startet den Worker als eigenen Prozess. Der macht nicht viel, als 5 Sekunden zu warten und dann wenn er beendet wird, sendet er ein Signal an den Hauptprozess def onFinished(self, i): welcher dann eine Meldung in die Konsole ausgibt. Nix besonderes, aber für mich ein Ansatz meine Aufgabe zu lösen.

    Quelle: https://stackoverflow.com/questions/29243692/pyqt5-how-to-make-qthread-return-data-to-main-thread

  • Das Thema muss sitzen, also versuche ich das etwas zu vertiefen. Gestern habe ich viel Stunden damit verbracht, mein Restic UI auf das waitingspinnerwidget umzubauen. Bis auf eine Funktion, die heute noch dran kommt, habe ich alles hinbekommen 🙂 Aber, wie immer habe ich danach Fragen. Ok, fangen wir mal an.

    @pyqtSlot(str)
    def onFinished(self, i):
        #print("Base caught finished, {}".format(i))
        print("Worker end", i)
    

    Wenn wir uns den Code anschauen, kommt die Frage auf, was macht @pyqtSlot(str) ? Ich probiere viel aus, also hatte ich das in meinem Code auskommentiert und mich gewundert, warum das immer noch funktioniert!? Ok, also Dokumentation lesen.

    Connecting a signal to a decorated Python method also has the advantage of reducing the amount of memory used and is slightly faster.

    Das klärt meine Frage aber immer noch nicht komplett. Auf stackoverflow.com, das folgende gefunden.

    For your case consider it as a good practice - denoting a method as a slot for some signal.

    Also muss ich mir jetzt merken, das es weniger Speicher verbraucht und etwas schneller ist. Gut, und es ordentlicher Programmierstil(?), wenn ich die Funktionen als abhängig von Signalen kennzeichne!? Ich denke, so passt es ganz gut.

    Sehr gute Anleitungen zum Thema findet man auch immer wieder auf realpython.com. Hier z.B. zum Thema QThread. Anhand des Beispieles habe ich meinen ersten QThread heute folgendermaßen dokumentiert.

        "First Thread with documentation"
        # Worker for restic_stats
        # Create a QThread object
        self.thread = QThread()
        # Create a worker object
        self.worker1 = Worker()
        # Move worker to the thread
        self.worker1.moveToThread(self.thread)
        # Connect signals and slots
        self.thread.started.connect(self.worker1.run)
        self.worker1.stats_finished[str].connect(self.restic_stats_finished)
        self.worker1.stats_error[str].connect(self.restic_stats_error)
        "First thread end"
    

    Dann stand ich sehr schnell vor dem Problem, was mache ich bei der nächsten Funktion, die einen QThread braucht?

        # Worker for restic_backup
        self.thread2 = QThread()
        self.worker2 = Worker2()
        self.worker2.moveToThread(self.thread2)
        self.thread2.started.connect(self.worker2.run)
        self.worker2.backup_finished[str].connect(self.restic_backup_finished)
        self.worker2.backup_error[str].connect(self.restic_backup_error)
    

    Ok, das war jetzt nicht so wirklich schwierig.

  • Und hier mal ein komplettes Beispiel.

    class Worker

    Wir legen den Worker an, das ist der Prozess der die Arbeit macht und etwas länger braucht.

    class Worker(QObject):
    
        """ Worker Class for Rest function stats"""
        stats_finished = pyqtSignal(str)
        stats_error = pyqtSignal(str)
    
        def __init__(self):
            super().__init__()
    
        def run(self):
            # Restic function
    
        try:
            # long running task
        except Exception:
            # Process don't successful, send signal
            self.stats_error.emit(result.stderr)
        else:
            # Process successful, send signal
            self.stats_finished.emit(result.stdout)
        finally:
            pass
    

    in class MainWindow

    class MainWindow(QMainWindow):
        def __init__(self):
        super().__init__()
    
        "First Thread with documentation"
        # Worker for restic_stats
        # Create a QThread object
        self.thread = QThread()
        # Create a worker object
        self.worker1 = Worker()
        # Move worker to the thread
        self.worker1.moveToThread(self.thread)
        # Connect signals and slots
        self.thread.started.connect(self.worker1.run)
        self.worker1.stats_finished[str].connect(self.restic_stats_finished)
        self.worker1.stats_error[str].connect(self.restic_stats_error)
        "First thread end"
    
        ###############################################
        # Process for restic_stats is finished
        ###############################################
    
        @pyqtSlot(str)
        def restic_stats_finished(self, i):
            # Signal from worker thread without an error
    
        Spinner.stop(self)
    
        self.thread.quit()
    
        ###############################################
        # Process for restic_stats when get an error
        ###############################################
    
        @pyqtSlot(str)
        def restic_stats_error(self, i):
            # Signal from worker thread with an error!
    
            Spinner.stop(self)
    

    Funktion restic_stats

    def restic_stats(self):
        # we start the worker thread
        self.thread.start()
    
        # we start waitingspinnerwidget
        Spinner.start(self)
    

    Ich wollte gerade schreiben, das folgendes sehr wichtig ist

    self.thread.quit()
    

    da fällt mir ein Fehler auf. Kurz ausprobiert und bingo, wenn der Prozess einen Error triggert, muss natürlich auch der Prozess beendet werden. Wenn man das nicht macht, macht das Programm nicht das was es soll. Der Grund ist, das der Prozess einfach immer weiter läuft. Er MUSS beendet werden. Ich gehe dann mal in meinem Programm alles ändern 🙂

    Ok, jetzt geht die Funktion auch zweimal hintereinander und gibt auch ordentlich den Fehler aus.

    Fazit

    Ich habe wieder sehr viel gelernt und hoffe das ich es auch richtig verstanden habe 😉 Hoffe das es dem ein oder anderen Anfänger hilft. Und falls hier ein Profi mitliest und hier Blödsinn steht bitte ich um einen Kommentar, damit ich das ändern kann. Es steht schon genug Blödsinn im Netz 🙂