QThread klemmt!?

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Jetzt bin ich dazu gekommen, mein vorheriges Beispiel mit den neuen Errungenschaften zu kombinieren. Jetzt präsentiere ich dir quasi das "Endergebnis" dieses Beispiels und hätte gern deinen "Segen", damit ich für mich sagen kann "Supi, damit kann ich schon mal arbeiten" und das ich dann eine gewisse Sicherheit im Gefühl bekomme. Den Quelltext siehst du weiter unten.

Wie du sehen wirst, habe ich die »QCoreApplication()«-Klasse gegen »QApplication« ausgetauscht. Damit das Beispiel nicht nur mit fünf Elementen aus einer Liste arbeitet, habe ich wieder die »xrange()« herangeholt. Das Beispiel soll ja auch seine Arbeit demonstrieren. Auch habe ich die »moveToThread()«-Methode herangezogen, damit die Arbeit auch in ein Thread verschoben wird. Oder sollte ich das nicht machen?

Was mir allerdings auffällt. Die GUI ruckelt zunächst einmal nicht. Das ist schon mal super. Aber der QTimer "stößt" die Arbeit ziemlich langsam an, oder etwa nicht? Ehe die 10.000 Items nun durchlaufen wurden, vergehen fast 2 Minuten. Ich habe mit der Stoppuhr meines Smartphones gemessen. Ist ein Bisschen zu "langsam" oder?

Code: Alles auswählen

import sys

from PyQt4.QtCore import QTimer, QObject, pyqtSignal, \
     QThread, Qt

from PyQt4.QtGui import QDialog, QLabel, QPushButton, \
     QApplication, QVBoxLayout, QTreeWidget, QTreeWidgetItem

def output(text):
    print "Output:",  text
 
class Counter(QObject):

    notify_progress = pyqtSignal(str)
    notify_item = pyqtSignal(object)
    finish_progress = pyqtSignal()
    fire_label = pyqtSignal(int)
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

        self.element = self.my_gen()

        self.timer = QTimer()

        # assoziiert increment() mit TIMEOUT Ereignis
        self.timer.setSingleShot(False)
        self.timer.timeout.connect(self.increment)
        self.timer.start()
 
    def my_gen(self):

        count = 0
               
        for name in xrange(10000):

            count += 1
            self.fire_label.emit(count)
            
            yield name
           
    def increment(self):

        try:
            self.notify_item.emit(next(self.element))

        except StopIteration:

            self.notify_progress.emit('Break the loop')
            self.finish_progress.emit()

            self.timer.stop()
        
    def stop(self):
        self.notify_progress.emit('Stop the loop')
        
        self.timer.stop()

        
class MyCustomDialog(QDialog):

    finish = pyqtSignal()

    def __init__(self, parent=None):
        QDialog.__init__(self, parent)
       
        layout = QVBoxLayout(self)
 
        self.tree = QTreeWidget(self)
        self.label = QLabel(self)
       
        self.pushButton_start = QPushButton("Start", self)
        self.pushButton_stopp = QPushButton("Stopp", self)
        self.pushButton_close = QPushButton("Close", self)
 
        layout.addWidget(self.label)
        layout.addWidget(self.tree)
        layout.addWidget(self.pushButton_start)
        layout.addWidget(self.pushButton_stopp)
        layout.addWidget(self.pushButton_close)
 
        self.pushButton_start.clicked.connect(self.on_start)
        self.pushButton_stopp.clicked.connect(self.on_finish)
        self.pushButton_close.clicked.connect(self.close)
 
    def fill_tree_widget(self, i):
        parent = QTreeWidgetItem(self.tree)
        self.tree.addTopLevelItem(parent)
        parent.setText(0, unicode(i))
        parent.setCheckState(0, Qt.Unchecked)
        parent.setFlags(parent.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable)
 
    def on_label(self, i):
         self.label.setText("Result: {}".format(i))
       
    def on_start(self):
         self.tree.clear()
         self.label.clear()
         
         task_thread = QThread(self)
         task_thread.work = Counter()

         task_thread.work.fire_label.connect(self.on_label)
         task_thread.work.notify_progress.connect(output)
         task_thread.work.notify_item.connect(self.fill_tree_widget)
         task_thread.work.finish_progress.connect(task_thread.quit)

         self.finish.connect(task_thread.work.stop)

         task_thread.work.moveToThread(task_thread)

         #task_thread.started.connect(task_thread.work)

         task_thread.finished.connect(task_thread.deleteLater)

         task_thread.start()

    def on_finish(self):
         self.finish.emit()
     
def main():
    app = QApplication(sys.argv)
    window = MyCustomDialog()
    window.resize(600, 400)
    window.show()
    sys.exit(app.exec_())
 
if __name__ == "__main__":
    main()
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Ohne Thread sollte es sogar etwas schneller sein. Die Lösung mit Thread, die Du jetzt gebaut hast, macht vermutlich wieder nicht das, was Du erwartet hattest. Mit Thread musst Du Dir genau über die Threadgrenzen im Klaren sein, sonst kommt da Verwurschteltes raus. Derzeit passiert folgendes:
- Counter.__init__ wird im Hauptthread aufgerufen (vor moveToThread)
- Timer in Counter wird in __init__ erstellt und gestartet --> Timer läuft im Hauptthread und wird daher gegen GUI-Ereignisschleife gesynct
- counter.moveToThread(...)
- `increment` wird im Subthread aufgerufen

Im Endergebnis hast Du in etwa folgende Konstellation für einen Timer-increment-Zyklus:
--> QTimer.TIMEOUT(Hauptthread) --> Subthread-eventloop --> increment+notify_item-Signal (Subthread) --> Hauptthread-eventloop --> output (Hauptthread) -->

Das ist unnötig kompliziert und weil es gegen die Ereignisschleife im Hauptthread synct (QTimer liegt dort) und die Signale zwischen den Threads dispatchen muss, auch so langsam.

Eine Möglichkeit der Vereinfachung wäre, alles in `Counter.__init__` in eine zweite Methode zu packen, welche mit `task_thread.started` aufgerufen wird. Damit ist der Timer im Subthread, was deutlich schneller die Liste abarbeitet. Aber Achtung: Wenn die Liste schneller abgearbeitet wird, heisst das auch, dass auf GUI-Seite mehr Elemente pro Zeit eingehängt und gezeichnet werden müssen, was wiederum zu GUI-Hängern führen kann. Generell lässt sich dazu nur sagen, dass der Subthread wenig Sinn ergibt, wenn dieser deutlich weniger pro Arbeitschritt zu tun hat als der Hauptthread (was hier der Fall ist: Subthread - next in Generator, Hauptthread - Eventverarbeitung + GUI-Elemente anlegen + neuzeichnen). Evtl. lässt sich sowas dann noch mit anderer Granularität beheben (z.B. mehrere Elemente pro 1x Neuzeichnen usw.)
Zweite Möglichkeit - Counter im Hauptthread lassen. Geht, wenn Counter nicht schneller werden muss und den Hauptthread nicht zu sehr belastet.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Hab gerade erst gesehen, dass Du `moveToThread` sehr spät nach den Signal-Slot-Verbindungen aufrufst. Damit wird Counter effektiv im Hauptthread abgearbeitet. Du musst hier beachten, dass `connect` threadaware ist. Wenn Du das auf ein Objekt im selben Thread anwendest, nimmt Qt eine Abkürzung und registriert den Slotaufruf direkt. Für ein Objekt im Fremdthread dispatcht es Signale in dessen QEventloop.

Du hast quasi Direktverbindungen hergestellt und dann das Objekt in einen anderen Thread verschoben. Direkte Aufrufe unterliegen immer dem selben Threadkontext, daher wird `increment` im Hauptthread aufgerufen.

Abhilfe - Signal-Slot-Verbindungen nach `moveToThread` erstellen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Das ich durch mein vorheriges Beispiel vieles durcheinander gebracht habe, ist mir gar nicht bewusst gewesen. Als du mich erst darauf hingewiesen und aufgeklärt hast, wurde mir das klar. Daher ein großes Danke an dieser Stelle.

Ich habe all deine Anmerkungen berücksichtigt, und noch eine kleine Sache hinzugetan. In Zeile 30 habe ich einen Interval auf den QTimer gesetzt. Ohne diesen Interval hat die GUI einfach zu sehr gehängt. Damit etwas Luft dazwischen bleibt, setzte ich einfach den Interval. Im Interval-Kontext entspricht 1.000 eine Sekunde, korrekt? Dann ist die Zahl 1 Millisekunde? Jedenfalls läuft die GUI-Seite füssig, und der Timer arbeitet etwas schneller :)

Code: Alles auswählen

import sys

from PyQt4.QtCore import QTimer, QObject, pyqtSignal, \
     QThread, Qt

from PyQt4.QtGui import QDialog, QLabel, QPushButton, \
     QApplication, QVBoxLayout, QTreeWidget, QTreeWidgetItem

def output(text):
    print "Output:",  text
 
class Counter(QObject):

    notify_progress = pyqtSignal(str)
    notify_item = pyqtSignal(object)
    finish_progress = pyqtSignal()
    fire_label = pyqtSignal(int)
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

    def init_object(self):

        self.element = self.my_gen()

        self.timer = QTimer()

        # assoziiert increment() mit TIMEOUT Ereignis
        self.timer.setSingleShot(False)
        self.timer.setInterval(1)
        self.timer.timeout.connect(self.increment)
        self.timer.start()
 
    def my_gen(self):

        count = 0
               
        for name in xrange(100000):

            count += 1
            self.fire_label.emit(count)
            
            yield name
           
    def increment(self):

        try:
            self.notify_item.emit(next(self.element))

        except StopIteration:

            self.notify_progress.emit('Break the loop')
            self.finish_progress.emit()

            self.timer.stop()
        
    def stop(self):
        self.notify_progress.emit('Stop the loop')
        
        self.timer.stop()

        
class MyCustomDialog(QDialog):

    finish = pyqtSignal()

    def __init__(self, parent=None):
        QDialog.__init__(self, parent)
       
        layout = QVBoxLayout(self)
 
        self.tree = QTreeWidget(self)
        self.label = QLabel(self)
       
        self.pushButton_start = QPushButton("Start", self)
        self.pushButton_stopp = QPushButton("Stopp", self)
        self.pushButton_close = QPushButton("Close", self)
 
        layout.addWidget(self.label)
        layout.addWidget(self.tree)
        layout.addWidget(self.pushButton_start)
        layout.addWidget(self.pushButton_stopp)
        layout.addWidget(self.pushButton_close)
 
        self.pushButton_start.clicked.connect(self.on_start)
        self.pushButton_stopp.clicked.connect(self.on_finish)
        self.pushButton_close.clicked.connect(self.close)
 
    def fill_tree_widget(self, i):
        parent = QTreeWidgetItem(self.tree)
        self.tree.addTopLevelItem(parent)
        parent.setText(0, unicode(i))
        parent.setCheckState(0, Qt.Unchecked)
        parent.setFlags(parent.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable)
 
    def on_label(self, i):
         self.label.setText("Result: {}".format(i))
       
    def on_start(self):
         self.tree.clear()
         self.label.clear()
         
         task_thread = QThread(self)
         task_thread.work = Counter()

         task_thread.work.moveToThread(task_thread)

         task_thread.work.fire_label.connect(self.on_label)
         task_thread.work.notify_progress.connect(output)
         task_thread.work.notify_item.connect(self.fill_tree_widget)
         task_thread.work.finish_progress.connect(task_thread.quit)

         self.finish.connect(task_thread.work.stop)
         
         task_thread.started.connect(task_thread.work.init_object)

         task_thread.finished.connect(task_thread.deleteLater)

         task_thread.start()

    def on_finish(self):
         self.finish.emit()
     
def main():
    app = QApplication(sys.argv)
    window = MyCustomDialog()
    window.resize(600, 400)
    window.show()
    sys.exit(app.exec_())
 
if __name__ == "__main__":
    main()
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Prima, jetzt haste fast eine Lehrbuchlösung (naja nach Aufräumen und Dubletten raus ;)). Und falls Du die Lust noch nicht verloren hast, kannste mit dem Wissen ja mal einen Threadpool mit Workerthreads bauen, welchen man Arbeit zuschanzen kann :D
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: www.was_ist_ein_Threadpool.de? Noch ein weitere, etwas allgemeinere Frage: Wozu dient dieser Threadpool? Damut man mehere Threads an und damit man mehrere Arbeiten "gleichzeitig" erledigen kann?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Wofür sowas gut ist, kannst Du unter BlackJacks URL nachlesen. Ob Du es für Deine Anwendung brauchst, kann ich Dir nicht sagen. Auch etwas nachzubauen, was es schon gibt (QThreadPool und zig andere nicht qt-basierte Implementationen), mag sinnfrei erscheinen. Oft erkennt man aber bei den komplexeren Entwurfsmustern deren Zwecks erst, wenn man selbst damit "gespielt" hat und lernt besser einzuschätzen, welches Problem das Entwurfsmuster eigentlich lösen möchte.
Z.B. kannst Du mit dem, was ich oben zur Ereignissteuerung geschrieben habe, selbst ein solches System bauen. Das wäre zunächst nicht besonders hübsch und elaboriert wie eines, was Mannjahre an Entwicklung hinter sich hat, funktioniert im Kern aber so wie alle anderen. Wenn Du das verstanden hast (was immer auch ohne Nachbauen geht, wenn man Doku/Code interpretieren kann), fällt Dir die Einschätzung darüber, was Qt z.B. bei direkten Signal-Slot-Verbindungen anders macht und warum die Reihenfolge plötzlich wichtig ist, sehr viel einfacher. Ohne dieses "Hintergrundwissen" überliest man gerne die kleinen aber wichtigen Doku-Hinweise, einfach weil man deren Bedeutung nicht einordnen kann. Die Qt-Doku ist übrigens hier besser als viele andere, weils sie recht explizit Dinge durchspricht, wo andere, wenn überhaupt, nur eine kurze trockene Notiz hinterlassen.
Antworten