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

Hallo Leute,

ich habe für euch ein kleines ausführbares Programm geschrieben. Ich möchte mit der QThread()-Klasse arbeiten, damit die GUI meiner Anwendung während der Arbeit nicht "einfriert". Zur Simulation möchte ich beliebig viele Items in die TreeWidget "laden". Diese Simulation lässt sich in der QThread()-Klasse bei xrange ändern.

Code: Alles auswählen

import sys

from PyQt4.QtCore import QThread, pyqtSignal, Qt
from PyQt4.QtGui import QVBoxLayout, QPushButton, QDialog, QProgressBar, QApplication, \
     QMessageBox, QTreeWidget, QTreeWidgetItem

class Test_Thread(QThread):
    notify_progress = pyqtSignal(str)

    def __init__(self, parent=None):
        QThread.__init__(self, parent)

    def create_items(self):
         for elem in xrange(190000):
             yield elem

    def run(self):
        for i in self.create_items():
            self.notify_progress.emit(str(i))

class MyCustomDialog(QDialog):
 
    def __init__(self):
        super(MyCustomDialog, self).__init__()
        layout = QVBoxLayout(self)
 
        self.tree = QTreeWidget(self)
        
        self.pushButton_start = QPushButton("Start", self)
        self.pushButton_close = QPushButton("Close", self)

        layout.addWidget(self.tree)
        layout.addWidget(self.pushButton_start)
        layout.addWidget(self.pushButton_close)
 
        self.test_task = Test_Thread()
        self.test_task.notify_progress.connect(self.fill_tree_widget)

        self.pushButton_start.clicked.connect(self.on_start)
        self.pushButton_close.clicked.connect(self.close)

    def fill_tree_widget(self, i):
        parent = QTreeWidgetItem(self.tree)
        parent.setText(0, "Number: {}".format(i))
        parent.setCheckState(0, Qt.Unchecked)
        parent.setFlags(parent.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable)

    def on_start(self):
        self.test_task.start()
 
def main():
    app = QApplication(sys.argv)
    window = MyCustomDialog()
    window.resize(600, 400)
    window.show()
    sys.exit(app.exec_())
 
if __name__ == "__main__":
    main()
Sobald ich aber die Anwendung ausführe, friert meine GUI trotzdem ein. Irgendwie will ich den Fehler nicht finden. Denn es tauchen auch keinerlei Fehlermeldungen auf. Ich bin mir ziemlich sicher, dass ich etwas übersehe, nur was?
BlackJack

@Sophus: Die friert nicht komplett ein, der Hauptthread ist nur *sehr* beschäftigt damit die 190.000 Signale zu bearbeiten und die GUI zu erweitern, so dass nur sehr wenig Luft bleibt etwas anderes zu tun. Sowie ich ein kleines `time.sleep()` in die Schleife in dem Thread eingebaut habe, war auch die GUI wieder bedienbar.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Aber ist ein QThread nicht für solche Situationen konzipiert worden? Meine Simulation könnte man erweitern, indem man ziemlich viele Datensätze aus der Datenbank lädt und diese dann über QTreeWidget ausgibt. Ich habe schon den Generator und den schnellen xrange() genommen. Den sleep() hast du also in die for-Schleife der run()-Funktion gepackt? Bremst ein sleep() nicht auch den Arbeitsprozess aus?
BlackJack

@Sophus: QThread ist dazu da etwas *parallel* zur GUI auszuführen. Was Dein Beispiel aber hauptsächlich macht ist ohne jegliche Pause Zahlen zu generieren, zum Hauptthread zu signalisieren, der daraufhin die GUI um Elemente erweitert. Die Hauptarbeit fällt also im Hauptthread in Konkurrenz zur Verarbeitung der Benutzerereignisse an.

`sleep()` ”bremst” den Thread aus in dem es ausgeführt wird und soll in diesem Beispiel simulieren das tatsächlich *Arbeit* im Thread passiert, die keinen direkten Einfluss auf den Hauptthread hat und damit parallel läuft. (Also in CPython ”semiparallel” wegen dem „global interpreter lock“.) Das laden von Datensätzen aus einer Datenbank nimmt ja auch Zeit in Anspruch. Mehr als einfach nur Zahlen zu genieren.

Es kann trotzdem sein, dass das so schnell geht, das das Verhältnis von den von diesem Thread generierten Signalen die Signale vom Benutzer soweit in den Schatten stellt, dass die so stark ausgebremst werden. Dann sollte man sich überlegen ob man wirklich für *jeden* Datensatz ein Signal feuert oder entweder nachdem alle geladen sind, oder in bestimmte feste ”Paketgrössen”, oder die Rate auf x mal in der Sekunde runter regelt.

Bei so grossen Anzahlen würde ich ausserdem zu einem `QTreeView` tendieren oder wenn das gar nicht wirklich eine Baumstruktur wird zu einem `QListView`.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

BlackJack brachte mich auf die Idee, die Daten nicht auf einmal zu laden, sondern in "Raten" - also Stück für Stück. Und so sieht mein Zwischenergebnis aus. Ich gebe zu, dass einige Variablen namentlich nicht ganz korrekt getroffen wurden.

Code: Alles auswählen

import sys
from time import sleep

from PyQt4.QtCore import QThread, pyqtSignal, Qt, QStringList
from PyQt4.QtGui import QVBoxLayout, QPushButton, QDialog, QProgressBar, QApplication, \
     QMessageBox, QTreeWidget, QTreeWidgetItem, QLabel

class Test_Thread(QThread):
    notify_progress = pyqtSignal(str)
    fire_label = pyqtSignal(str)

    def __init__(self, parent=None):
        QThread.__init__(self, parent)

        self.total = 100000
        self.count = 0
        self.x = (self.total/100*1)
        self.a = self.x
        
    def create_items(self):
         for elem in range(self.total):
             yield elem

    def run(self):
        for i in self.create_items():
            self.notify_progress.emit(str(i))
            self.count += 1
            self.fire_label.emit(str(self.count))

            if self.count == self.x:
                self.x += self.a
                sleep(0.2)

class MyCustomDialog(QDialog):
 
    def __init__(self):
        super(MyCustomDialog, self).__init__()
        layout = QVBoxLayout(self)
 
        self.tree = QTreeWidget(self)
        self.label = QLabel(self)
        
        self.pushButton_start = QPushButton("Start", self)
        self.pushButton_close = QPushButton("Close", self)

        layout.addWidget(self.label)
        layout.addWidget(self.tree)
        layout.addWidget(self.pushButton_start)
        layout.addWidget(self.pushButton_close)
 
        self.test_task = Test_Thread()
        self.test_task.notify_progress.connect(self.fill_tree_widget)
        self.test_task.fire_label.connect(self.on_label)

        self.pushButton_start.clicked.connect(self.on_start)
        self.pushButton_close.clicked.connect(self.close)

    def fill_tree_widget(self, i):
        parent = QTreeWidgetItem(self.tree)
        self.tree.addTopLevelItem(parent)
        parent.setText(0, "Number: {}".format(i))
        #self.tree.insertTopLevelItem(i, QTreeWidgetItem(i))
        parent.setCheckState(0, Qt.Unchecked)
        parent.setFlags(parent.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable)

    def on_start(self):
        self.test_task.start()

    def on_label(self, i):
        self.label.setText("Treffer: {}".format(i))
 
def main():
    app = QApplication(sys.argv)
    window = MyCustomDialog()
    window.resize(600, 400)
    window.show()
    sys.exit(app.exec_())
 
if __name__ == "__main__":
    main()
Was habe ich getan? Zunächst habe ich in Zeile 15-18 Variablen erstellt und dort die Zahlen gespeichert. In Zeile 17 errechne ich den Prozentwert. Hier soll also 1 Prozent von total errechnet werden. In Zeilen 30-32 habe ich dann eine If-Abfrage, zusammen mit einem Zähler, eingebaut. Wenn der Zähler das Ergebnis des zuvor errechneten Prozentwert erreicht, dann soll eine kleine Pause eingelegt werden. Hier sind es 0.2 Millisekunden. In diesem Beispiel bedeutet das, dass alle 1.000 Items eine Pause eingelegt werden soll.

Aber vielleicht habt ihr eine bessere Lösung? Denn mit meiner Lösung bin ich nicht ganz zufrieden. Denn die Prozentwert-Formel hat von Natur aus die Eigenschaft, je kleiner die ausgehende Zahl (self.total), desto kleiner fallen die Ergebnisse (self.x) aus. Man stelle sich mal vor, wie kleinschrittig die Ergebnisse werden, wenn nur 100 oder gar 10 Items geladen werden. Man könnte meinen, eine weitere If-Abfrage einzubauen, die prüft, ob self.total größer/kleiner als.... ist, und das man dementsprechend das Laden anpasst. Zum Beispiel, wenn self.total nur 100 oder gar 500 ist, dann könnte man den Zähler überspringen und die Items in einem Rutsch laden. Allerdings halte ich das nicht für eine gute Idee. Denn der Test_Thread() hat so schon viel zu tun. Noch weitere If-Abfragen wirken doch eher ausbremsend, nicht wahr?

Ich bin auf eure Meinung und Kritik gespannt.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Du benutzt leider QThread falsch (wie auch fast alle anderen älteren Bsp. dazu, die im Netz rumgeistern). Dieses Missverständnis im Umgang mit QThread wird inzwischen seitens Qt ellenlang dokumentiert und mit Beispielen, wie man es richtig macht, geradegerückt.

kurze Erklärung dazu:
QThread ist eine Klasse, welche den nebenläufigen Code unter `QThread::run` kapselt. Das Threadobjekt selbst jedoch liegt im "Erzeugerthread", heisst auch alle Instanzattribute (QThread ist nicht der Thread selbst sondern nur ein Threadcontainer!). Das führt dazu, dass Deine Zugriffe auf `.create_items`, `.count`, `.a` und `.x` aus `.run` threadübergreifend sind. Das geht solange gut, wie die Zugriffe atomar sind bzw. nur von `.run` aus passieren. Zusätzlich sind QObjects threadaffin, d.h. sie "kleben" am Erzeugerthread und sollten nicht ohne weiteres aus einem anderen Thread heraus benutzt werden. Grund hierfür ist Qts Objektbaum und Signal-Slot-Mechanismus, beides ist intern threadgebunden. Man kann ein QObject trotzdem verschieben mittels `void QObject::moveToThread(QThread *targetThread)`, dabei wird das Objekt "umgehängt".
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo jerch,

zunächst erst einmal besten Dank. Allerdings bin ich ein wenig verwirrt.

Bezüglich der Attribute beziehst du dich sicherlich in meinem Beispiel-Quelltext auf Zeilen 15-18, richtig? Was mich hierbei verwirrt, ist, dass die Variablen als Attribute "deklariert" werden. Ist es nicht üblich, dass eine Klasse immer Attribute hat? Spätestens, wenn man einer Klasse ein oder mehrere Argumente übergibt. Erst dann müssen die zu erwartenden Argumente in der Klasse als Attribute deklariert werden, damit man überhaupt innterhalb einer Klasse Zugriff auf die Argumente bekommt und die Information verarbeiten kann. Gut, in meinem Fall wird der Test_Thread()-Klasse nichts übergeben. Aber ich hätte nicht gedacht bzw. damit gerechnet, dass dies ein Problem darstellt. Wenn ich dich also richtig verstanden habe, möchtest du vorschlagen, diese Variablen lokal unter der run()-Funktion zu behandeln? Was mich weiterhin noch verwirrt, ist, dass du die create_items()-Funktion ins Visier genommen hast. Wolltest du dabei auf die Kapselung von Logik und Daten hinaus? Das heißt, die create_items()-Funktion außerhalb der Test_Thread()-Klasse und MyCustomDialog()-Klasse zu erstellen?

Etwas so?

Code: Alles auswählen

import sys
from time import sleep
 
from PyQt4.QtCore import QThread, pyqtSignal, Qt, QStringList
from PyQt4.QtGui import QVBoxLayout, QPushButton, QDialog, QProgressBar, QApplication, \
     QMessageBox, QTreeWidget, QTreeWidgetItem, QLabel

def create_items(total):
     for elem in range(total):
         yield elem
             
class Test_Thread(QThread):
    notify_progress = pyqtSignal(str)
    fire_label = pyqtSignal(str)
 
    def __init__(self, parent=None):
        QThread.__init__(self, parent)
 
    def run(self):
        total = 100000
        count = 0
        x = (total/100*1)
        a = x
        for i in create_items(total):
            self.notify_progress.emit(str(i))
            count += 1
            self.fire_label.emit(str(count))
 
            if count == x:
                x += a
                sleep(0.2)
 
class MyCustomDialog(QDialog):
 
    def __init__(self):
        super(MyCustomDialog, self).__init__()
        layout = QVBoxLayout(self)
 
        self.tree = QTreeWidget(self)
        self.label = QLabel(self)
       
        self.pushButton_start = QPushButton("Start", self)
        self.pushButton_close = QPushButton("Close", self)
 
        layout.addWidget(self.label)
        layout.addWidget(self.tree)
        layout.addWidget(self.pushButton_start)
        layout.addWidget(self.pushButton_close)
 
        self.test_task = Test_Thread()
        self.test_task.notify_progress.connect(self.fill_tree_widget)
        self.test_task.fire_label.connect(self.on_label)
 
        self.pushButton_start.clicked.connect(self.on_start)
        self.pushButton_close.clicked.connect(self.close)
 
    def fill_tree_widget(self, i):
        parent = QTreeWidgetItem(self.tree)
        self.tree.addTopLevelItem(parent)
        parent.setText(0, "Number: {}".format(i))
        #self.tree.insertTopLevelItem(i, QTreeWidgetItem(i))
        parent.setCheckState(0, Qt.Unchecked)
        parent.setFlags(parent.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable)
 
    def on_start(self):
        self.test_task.start()
 
    def on_label(self, i):
        self.label.setText("Treffer: {}".format(i))
 
def main():
    app = QApplication(sys.argv)
    window = MyCustomDialog()
    window.resize(600, 400)
    window.show()
    sys.exit(app.exec_())
 
if __name__ == "__main__":
    main()
Vermutlich habe ich deine Anmerkung falsch verstanden und alles verschlimmbessert.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Nochmal hallo jerch,

ich habe deine Anmerkung zum Object() vollkommen übersehen, und habe diesbezüglich etwas gebastelt. Allerdings will das Skript nicht funktionieren. Sobald ich auf die Stat-Schaltfläche klicke, bekomme ich die Meldung in Form einer Messagebox, dass pythonw.exe nicht mir funktioniert. Daraufhin bricht dann alles ab.

Code: Alles auswählen

import sys
from time import sleep
 
from PyQt4.QtCore import QThread, pyqtSignal, Qt, QStringList, QObject
from PyQt4.QtGui import QVBoxLayout, QPushButton, QDialog, QProgressBar, QApplication, \
     QMessageBox, QTreeWidget, QTreeWidgetItem, QLabel
 
def create_items(total):
     for elem in range(total):
         yield elem
             
class WorkObject(QObject):

    notify_progress = pyqtSignal(str)
    fire_label = pyqtSignal(str)

    def run(self):
        total = 100000
        count = 0
        x = (total/100*1)
        a = x
        for i in create_items(total):
            self.notify_progress.emit(str(i))
            count += 1
            self.fire_label.emit(str(count))
 
            if count == x:
                x += a
                sleep(0.2)
 
class MyCustomDialog(QDialog):
 
    def __init__(self):
        super(MyCustomDialog, self).__init__()
        layout = QVBoxLayout(self)
 
        self.tree = QTreeWidget(self)
        self.label = QLabel(self)
       
        self.pushButton_start = QPushButton("Start", self)
        self.pushButton_close = QPushButton("Close", self)
 
        layout.addWidget(self.label)
        layout.addWidget(self.tree)
        layout.addWidget(self.pushButton_start)
        layout.addWidget(self.pushButton_close)
  
        self.pushButton_start.clicked.connect(self.on_start)
        self.pushButton_close.clicked.connect(self.close)
 
    def fill_tree_widget(self, i):
        parent = QTreeWidgetItem(self.tree)
        self.tree.addTopLevelItem(parent)
        parent.setText(0, "Number: {}".format(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):
        task_thread = QThread()
        
        work_object = WorkObject()
        work_object.moveToThread(task_thread)
        
        task_thread.started.connect(work_object.run)
        task_thread.finished.connect(self.close)
        task_thread.start()
 
def main():
    app = QApplication(sys.argv)
    window = MyCustomDialog()
    window.resize(600, 400)
    window.show()
    sys.exit(app.exec_())
 
if __name__ == "__main__":
    main()
Was habe ich getan? Du schriebst das Stichwort »Threadaffinität«. Zunächst habe ich in Zeile 12 eine neue »CreateItems()«-Klasse definiert, die von »QObject« ableitet. In dieser neuen Klasse habe ich eine »run«-Funktion definiert. Dort liegt die eigentliche Arbeit.
In Zeilen 61-69 habe ich die »on_start()«-Funktion umgeschrieben. Hier wird alles auf lokaler ebene erzeigt.

Um also die sogenannte »Threadaffinität« eines Objektes zu ändern, bediene ich mich der »moveToThread()«-Methode. Das heißt: hier gehe ich dazu über, dass ein Objekt (»CreateItems()«) vom eigenen Thread in einen anderen ( »QThread()«) zu schieben bzw. zu bewegen.

Ich hoffe, ich habe dich hierbei richtig verstanden, in den QThread richtig verwendet.

Allerdings will mir dieses Beispiel nicht richtig gelingen. Wie schon oben erwähnt, stürzt mir pythonw.exe nach dem Klick auf der Start-Schaltfläche ab.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Das Problem mit dem Absturz von pythonw.exe hat sich erledigt.

Ich vergaß im vorherigen Beispiel-Quelltext in Zeile 61 der »QThread()«-Klasse was zu übergeben, und zwar den self.

Allerdings verstehe ich noch nicht ganz, was nun der Vorteil gegenüber der älteren Version ist. Wieso musste ich eine weitere »WorkObject()«-Klasse definieren, die von der »QObject()«-Klasse ableitet? Wo ist nun der Vorteil zu finden? Vermutlich fehlt mir ein kleiner Gedankensprung, um die ganze Dimension zu verstehen und zu verinnerlichen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Manchmal bin ich echt ein Dussel. Ich sollte nicht immer so schnell abschicken. Der vorherige Quelltext kann nicht funktionieren, weil die Signale und Slots nicht verbunden wurden. Hier das vollständige und ausführbare Skript.

Code: Alles auswählen

import sys
from time import sleep
 
from PyQt4.QtCore import QThread, pyqtSignal, Qt, QStringList, QObject
from PyQt4.QtGui import QVBoxLayout, QPushButton, QDialog, QProgressBar, QApplication, \
     QMessageBox, QTreeWidget, QTreeWidgetItem, QLabel
 
def create_items(total):
     for elem in range(total):
         yield elem
             
class WorkObject(QObject):
 
    notify_progress = pyqtSignal(object)
    fire_label = pyqtSignal(object)

    def __init__(self):
        super(WorkObject, self).__init__()
 
    def run(self):
        total = 190000
        count = 0
        x = (total/100*1)
        a = x
        for i in create_items(total):
             self.notify_progress.emit((i))
             count += 1
             self.fire_label.emit((count))

             if count == x:
                  x += a
                  sleep(1)
 
class MyCustomDialog(QDialog):
 
    def __init__(self):
        super(MyCustomDialog, self).__init__()

        self.work_object = WorkObject()
        
        layout = QVBoxLayout(self)
 
        self.tree = QTreeWidget(self)
        self.label = QLabel(self)
       
        self.pushButton_start = QPushButton("Start", self)
        self.pushButton_close = QPushButton("Close", self)
 
        layout.addWidget(self.label)
        layout.addWidget(self.tree)
        layout.addWidget(self.pushButton_start)
        layout.addWidget(self.pushButton_close)
 
        self.pushButton_start.clicked.connect(self.on_start)
        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):
          task_thread = QThread(self)

          self.work_object.fire_label.connect(self.on_label)
          self.work_object.notify_progress.connect(self.fill_tree_widget)
          
          self.work_object.moveToThread(task_thread)
          
          task_thread.started.connect(self.work_object.run)
          task_thread.start()
     
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:
Ich habe mich auf alle Attribute Deiner QThread-Subklasse bezogen. Eine QThread-Subklasse lädt dazu ein, diese zu benutzen, wie Du oben gezeigt hast. Das passt aus OOP-Sicht auch (also ohne den Threadbezug). Aber leider passen die typischen Anwendungspattern bei QThread nicht mehr, eben weil der Threadkontext zu beachten ist. Wichtige Regeln dabei:
  1. Alles unter `run` wird im Subthread ausgeführt. Alles ausserhalb (inkl. Ctor!) wird im Elternthread ausgeführt.
  2. QOjects sind threadaffin! (Besonderheit, die so sonst nicht/kaum gibt bei C++/Pythonobjekten)
  3. Aus 1) und 2) folgt, dass QOject-Attribute des Konstruktors im Elternthread sind.
Punkt 2.) ist deshalb ungewöhnlich, da es eine Art Speicher-zu-Threadabhängigkeit einführt, was normalerweise nicht der Fall ist - Threads sind in erster Linie eine Art Ausführungsslot und kein Memoryslot. Auch ist diese Kopplung der QObjects an einen Thread eher lose und schlägt nur bei Funktionalitäten durch, welche explizit threadabhängig sind (Traversieren des Objektbaumes, Signal-Slot-Verbindungen, Qt-Objekt-Introspektionen), was das debuggen des eigenen Codes erschwert (der eigene Code läuft dann in der Regel problemlos, aber tief in der Vererbungshierachie schlägt ein Subcall fehl.)

Nun zum Problem mit den Attributen - wenn Du in Deiner QThread-Subklasse im Konstruktor ein QObject-Exemplar erstellst und an ein Attribut klemmst, kann dessen Threadaffinität problematisch in der Benutzung unter `run` werden. Wie gesagt, meistens klappts, aber bestimmte Aktionen schlagen definitiv fehl (probier einfach mal, ein neues QObject-Exemplar als Kind des Attribut-Exemplars anzulegen...). Richtig haarig sind dann so Versuche, einen Timer, der im Konstruktor erstellt wurde, unter run nutzen zu wollen. Das ist super aus OOP-Entwurfssicht, geht aber krachen mit QThread.

Von Deinen neuen Posts habe ich mir nur den Thread-Code des letzten angesehen. Jepp - genau das ist die Idee, wie man mit QThread hantieren sollte. Dabei kann das `WorkObject` auch sehr viel kompexer sein und z.B. auf Signale reagieren (z.B. zum Stoppen der Threadausführung). QThread bringt für diesen Fall schon den Eventloop mit, welcher nur noch mit `.exec()` gestartet werden muss. Ausgehende Signale funktionieren auch ohne eigenen Eventloop im Subthread.

Hier ist mal eine etwas ausführlichere Darstellung dazu: https://wiki.qt.io/Threads_Events_QObje ... event_loop
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Besten dank für deine ausführliche Erklärung, auch wenn ich einiges noch nicht ganz verstanden habe.

Ich hätte noch ein paar kleine Fragen an dich. Du schriebst, dass in meinem Fall die »WorkObject()«-Klasse auf Signale reagieren kann. Zum Beispiel, wenn ich die Threadausführung anhalten/unterbrechen möchte. Dazu habe ich mein vorheriges Beispiel-Programm erweitert.

Code: Alles auswählen

import sys
from time import sleep
 
from PyQt4.QtCore import QThread, pyqtSignal, Qt, QStringList, QObject
from PyQt4.QtGui import QVBoxLayout, QPushButton, QDialog, QProgressBar, QApplication, \
     QMessageBox, QTreeWidget, QTreeWidgetItem, QLabel
 
def create_items(total):
     for elem in range(total):
         yield elem
             
class WorkObject(QObject):
 
    notify_progress = pyqtSignal(object)
    fire_label = pyqtSignal(object)
    finished = pyqtSignal()

    def __init__(self):
        super(WorkObject, self).__init__()

        self.stopp_it = 0
 
    def run(self):
        total = 190000
        count = 0
        x = (total/100*1)
        a = x
        for i in create_items(total):
             self.notify_progress.emit((i))
             count += 1
             self.fire_label.emit((count))

             if count == x:
                  x += a
                  sleep(1)
                  
             if self.stopp_it == 1:
                  self.stopp = 0
                  self.finished.emit()
                  break

    def stopp(self):
         self.stopp_it = 1
         

class MyCustomDialog(QDialog):

    finish = pyqtSignal()

    def __init__(self, parent=None):
        super(MyCustomDialog, self).__init__(parent)

        self.work_object = WorkObject()
        
        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):
          task_thread = QThread(self)

          self.work_object.fire_label.connect(self.on_label)
          self.work_object.notify_progress.connect(self.fill_tree_widget)
          self.work_object.finished.connect(task_thread.quit)

          self.finish.connect(self.work_object.stopp)
          
          self.work_object.moveToThread(task_thread)
          
          task_thread.started.connect(self.work_object.run)
          
          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()
Was habe ich getan? Ich habe in Zeile 42-43 in der »WorkObject()«-Klasse eine »stopp()«-Funktion hinzugefügt. Gleich dazu habe ich in der besagten Klasse ein »stopp_it«-Attribut hinzugefügt. An dieser Stelle taucht auch schon meine erste Frage auf. Du siehst hier, dass ich ein Attribut erstellt habe, welches threadübergreifend ist, richtig? Nach deiner Überlegung wäre dies aber falsch. Mich lässt diese Problematik der Attributinstanzierung keine Ruhe. Oder aber, ich habe dich wie üblich nur falsch verstanden.

Im nächsten Schritt habe ich im Konstrukt der »MyCustomDialog()«Klasse ein Signal erzeugt. Um dieses Signal abfeuern zu können, habe ich in Zeilen 99-100 eine »on_finish()«-Funktion erstellt. Damit die »WorkObject()«-Klasse auf das Signal reagieren kann, muss eine Verbindung hergestellt werden. Dies tat ich dann in der »on_start()«Funktion. Das »finish«-Signal wird also mit der »stopp()«-Funktion der »WorkObject()«-Klasse verbunden. Die zweite Frage, die folgt: Ist der Weg so richtig? Irgendwie wirkt dieser Weg sehr umständlich. Denn immerhin muss zunächst ein Signal im Konstrukt erstellt werden, anschließend eine weitere Funktion, um dass Abfeuern der Signale unterzubringen, und im letzten Schritt muss das Signal verbunden werden. Sieht eher aus wie von hinten durch die Brust ins Auge.

Wenn man das Programm ausführt, klappt das alles wie geplant. Wenn ich den Vorgang stoppe/unterbreche, und anschließend wieder auf Start klicke, dann bekomme ich folgende Traceback-Meldung:
Traceback (most recent call last):
File "C:\Users\Sophus\Desktop\thread_correct.py", line 91, in on_start
self.finish.connect(self.work_object.stopp)
TypeError: connect() slot argument should be a callable or a signal, not 'int'
Meine dritte und letzte Frage lautet: Was habe ich laut dem Traceback falsch gemacht? Ich komme der Fehlermeldung nicht auf die Spur.
BlackJack

@Sophus: Du versuchst das Signal mit einer ganzen Zahl als Empfänger zu erstellen statt eine Methode anzugeben. `WorkObject`-Exemplare haben nur solange eine `stopp`-Methode bis Du die in Zeile 38 durch die Zahl 0 ersetzt.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Du hast Recht, dieser Fehler ist mir gar nicht aufgefallen. Ich wollte in Zeile 38 keine Methode aufrufen, sondern den Wert des Attributes ändern, damit sie die Zahl 0 bekommt. Allerdings verhält sich mein Programm "komisch", wenn ich nach dem STOPP wieder auf Start klicke. Das Programm reagiert nicht.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ich habe das Problem insofern gelöst, dass ich die Erzeugung der »QThread()«-Klasse innerhalb der »MyCustomDialog()«-Klasse vornehme. Zuvor wurde die »QThread()«-Klasse lokal in der »on_start()«-Funktion erzeugt.

Allerdings will mir mein Stopp-Mechanismus nicht ganz greifen. Wenn ich also das Programm gestartet und mit einem Klick auf Start den Arbeitsvorgang gestartet habe und anschließend gestpoppt habe, und wieder mit einem Klick auf Start starte, und dann stoppen will, will es nicht klappen. Ich muss dann mehrmals auf die Stopp-Schaltfläche klicken, ehe der Thread seine Arbeit einstellt.

Kurzum:

1. Vorgang: Start und Stopp klappen super
2. Vorgang: Startet super, aber Stopp will nicht sofort greifen - erst nach mehrmaligen Klicken.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Was Dein Stopp gerade macht:

Beim ersten Start wird `stopp()` vor `moveToThread` gebunden, ergo beim Stoppen im Mainthread aufgerufen. Daher funktioniert es einmal.

Beim zweiten Mal ist das Objekt bereits in einem anderen Thread, was `.connect` veranlasst, eine queued connection zu platzieren. Da aber im Subthread kein Eventloop läuft, kommt da nix an.

Damit das korrekt funktioniert, musst Du Dein WorkObject auf ereignisgesteuert umstellen, sprich die run-sleep-Logik loswerden, alles über den threadlokalen Eventloop steuern (z.B. per QTimer) und den Loop auch starten (per `exec_`).

NB: Auch sollte man Threads korrekt beenden, was bei bestehenden threadübergreifenden Signal-Slot-Verbindungen mit `deleteLater` gemacht werden muss, damit pending events nicht einen hängenden Zeiger triggern.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch, erst einmal vielen Dank.

Ich glaube, ich habe alles verschlimmbessert. Hier mein Beispiel-Quelltext.

Code: Alles auswählen

import sys
 
from PyQt4.QtCore import QThread, pyqtSignal, Qt, QStringList, QObject, QTimer
from PyQt4.QtGui import QVBoxLayout, QPushButton, QDialog, QProgressBar, QApplication, \
     QMessageBox, QTreeWidget, QTreeWidgetItem, QLabel
 
def create_items(total):
     for elem in range(total):
         yield elem
             
class WorkObject(QObject):
 
    notify_progress = pyqtSignal(object)
    fire_label = pyqtSignal(object)
    finished = pyqtSignal()

    def __init__(self):
        QObject.__init__(self)

    def add_items(self):
         total = 190000
         count = 0
         
         for i in create_items(total):
            self.notify_progress.emit((i))
            count += 1
            self.fire_label.emit((count))
 
    def run(self):
         self.timer = QTimer()

         # I set the single shot timer on False,
         # because I don't want the timer to fires only once,
         # it should fires every interval milliseconds
         self.timer.setSingleShot(False)
         self.timer.timeout.connect(self.add_items)
         self.timer.start(500)

    def stop(self):
         self.timer.stop()
         

class MyCustomDialog(QDialog):

    finish = pyqtSignal()

    def __init__(self, parent=None):
        QDialog.__init__(self, parent)

        self.work_object = WorkObject()
        
        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)

         self.work_object.fire_label.connect(self.on_label)
         self.work_object.notify_progress.connect(self.fill_tree_widget)
         self.work_object.finished.connect(task_thread.deleteLater)

         self.finish.connect(self.work_object.stop)

         self.work_object.moveToThread(task_thread)

         task_thread.started.connect(self.work_object.run)

         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()
Was habe ich getan? Zunächst einmal habe ich das Timer-Modul entfernt. Dann ging ich dazu über in Zeile 29 die »run()«-Methode der »WorkObject()«-Klasse umzugestalten. In dieser besagten Methode erzeuge ich den QTimer(). In Zeile 36 stelle ich eine Verbindung zur »add_items()«-Funktion her. Da ich nicht will, dass der QTimer nur einmal feuert, habe ich in Zeile 35 den QTimer() mittels der »setSingleShot()«-Methode auf False gesetzt. Der Timer soll sich in einem Interval mit der Funktion verbinden und feuern. In der »stop()«-Methode der »WorkObject()«-Klasse will ich den QTimer() stoppen. Des Weiteren habe ich in Zeile 88 die »deleteLater()«-Methode der »QThread()«-Klasse angewendet. Zuvor hatte ich die »quit()«-Methode, anstatt »deleteLater()«-Methode angewendet - in der Hoffnung, dass durch die »quit()«-Methode der die »Qthread()«-Klasse der besagte Thread auch wirklich beendet wird.

Verschlimmbessert wurde mein Beispiel in dem Fall, weil der Vorgang mächtig ruckelt, und der Stopp gar nicht reagiert.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Die Sache mit dem QTimer ist schon die richtige Idee, um die Threadarbeit auf Ereignissteuerung umzustellen. Nur hast Du letzteres nicht gemacht, sondern einfach alle Arbeit in `add_items` reingeklatscht und lässt das Ganze alle 500ms wiederholen. Du musst schon nachvollziehen, was Dein Code eigentlich macht und die Umformung richtig vornehmen. Als Tipp dazu - überleg mal, wie Du eine simple Endlos-Zählschleife unter Nutzung von QTimer umstellen würdest.

Damit der Hauptthread responsiv bleibt, musst Du u.a. die Ereignislast reduzieren, sonst ist auch der Hauptthread zu 99% mit der Abarbeitung der Ereignisse beschäftigt. Und damit der Subthread sofort beginnt, solltest Du den Timer auf 0 setzen.

Auch ist das Verschieben des dialogweiten `worker_object` in ein neues Thread-Exemplar in `on_start` mehr als ungünstig und funktioniert auch beim 2. Mal nicht mehr (`moveToThread` ist selbst nicht threadsafe und daher nur für threadlokale Objekte erlaubt).

Leider fehlen PyQt die QtConcurrent- und Future-Implementationen für simple nebenläufige Aktionen, da diese templatisiert in C++ sind und sich nicht in prototypisieren lassen. Man kann sich aber selbst weiterhelfen, hier mal als Idee:

Code: Alles auswählen

from time import sleep
from PyQt4.QtCore import QThread, QObject, QTimer, pyqtSignal, QCoreApplication


class TaskThread(QThread):
    '''
    Thread container for a Task.
    Overrides `run` to initialize and run a single task object.
    '''
    def __init__(self, obj, args, kwargs, parent=None):
        QThread.__init__(self, parent)
        obj.moveToThread(self)
        self.obj = obj
        self.args = args
        self.kwargs = kwargs

    def run(self):
        try:
            self.obj.init(*self.args, **self.kwargs)
        except:
            pass
        self.obj.result = self.obj.run()
        QCoreApplication.processEvents()

    def _close(self):
        self.quit()
        self.wait()
        self.obj.thread = None
        self.obj.deleteLater()
        self.deleteLater()


class Task(QObject):
    '''
    Task - a `run` method based self containing theaded task.
    Executes the `init` and `run` method within a subthread.
    The provided `processEvents` method should be called once in a while
    in `run` to be able to catch events and trigger slots.
    Any result of `run` will be emitted via the `finished` signal.
    '''
    finished = pyqtSignal(object)
    def __init__(self, *args, **kwargs):
        QObject.__init__(self, None)
        self.result = None
        self.thread = TaskThread(self, args, kwargs, None)
        self.thread.finished.connect(self.thread._close)
        self.thread.finished.connect(self._finished)

    def processEvents(self):
        QCoreApplication.processEvents()

    def start(self):
        self.thread.start()

    def _finished(self):
        self.finished.emit(self.result)


class SimpleRunner(Task):
    '''
    Container to run functions threaded.
    '''
    def init(self, func, fargs, fkwargs):
        self.func = func
        self.fargs = fargs
        self.fkwargs = fkwargs

    def run(self):
        return self.func(*self.fargs, **self.fkwargs)


def to_be_threaded():
    workload = ['puuh', 'this is hard to compute', '...', 'we are', 'almost there', '... done']
    for s in workload:
        print 'simple runner:', s
        sleep(1)
    return 42


class TaskWithSignalsAndSlots(Task):
    '''
    Task example with signal and slot dispatching.
    '''
    counter = pyqtSignal(int)
    def init(self):
        self.keepRunning = True

    def run(self):
        counter = 0
        while self.keepRunning:
            counter += 1
            self.counter.emit(counter)
            sleep(1)
            self.processEvents()

    def stop(self):
        print 'slot stop() triggered'
        self.keepRunning = False

def simple_result(result):
    print 'result of the the long awaited calculation:', result

def outputCounter(counter):
    print 'signal counter(int) from task', counter


if __name__ == '__main__':
    app = QCoreApplication([])
    
    # simple threaded functions
    simple = SimpleRunner(to_be_threaded, [], {})
    simple.finished.connect(simple_result)
    simple.start()

    # more sophisticated example with signals and slots
    task = TaskWithSignalsAndSlots()
    task.counter.connect(outputCounter)
    task.finished.connect(QCoreApplication.instance().quit)
    timer = QTimer()
    timer.setInterval(7000)
    timer.timeout.connect(task.stop)
    timer.start()
    task.start()

    app.exec_()
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch:
Besten Dank. Ich habe deine Ausführung mal als Blaupause genommen und versucht meinen Quelltext drüber zu spulten. Vermutlich ist dies mir misslungen, aber wir werden es ja sehen. Wie immer habe ich ein paar frage, die ich am Ende des Beitrages stellen werde.

Code: Alles auswählen

import sys
from time import sleep

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

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

def create_items(total):
    for elem in range(total):
        yield elem

class TaskThread(QThread):
    '''
   Thread container for a Task.
   Overrides `run` to initialize and run a single task object.
   '''
    def __init__(self, obj, args, kwargs, parent=None):
        QThread.__init__(self, parent)
        obj.moveToThread(self)
        self.obj = obj
        self.args = args
        self.kwargs = kwargs
 
    def run(self):
        try:
            self.obj.init(*self.args, **self.kwargs)
        except:
            pass
        self.obj.result = self.obj.run()
        QApplication.processEvents()
 
    def _close(self):
        self.quit()
        self.wait()
        self.obj.thread = None
        self.obj.deleteLater()
        self.deleteLater()


class Task(QObject):
    '''
   Task - a `run` method based self containing theaded task.
   Executes the `init` and `run` method within a subthread.
   The provided `processEvents` method should be called once in a while
   in `run` to be able to catch events and trigger slots.
   Any result of `run` will be emitted via the `finished` signal.
   '''
    finished = pyqtSignal(object)
    def __init__(self, *args, **kwargs):
        QObject.__init__(self, None)
        self.result = None
        self.thread = TaskThread(self, args, kwargs, None)
        self.thread.finished.connect(self._finished)
 
    def processEvents(self):
        QApplication.processEvents()
 
    def start(self):
        self.thread.start()
 
    def _finished(self):
        self.finished.emit(self.result)

class TaskWithSignalsAndSlots(Task):
    '''
   Task example with signal and slot dispatching.
   '''
    notify_counter = pyqtSignal(object)
    notify_item = pyqtSignal(object)
    
    def init(self):
        self.keep_running = True
 
    def run(self):

        total = 1000

        x = (total/100*1)

        a = x

        counter = 0
        for item in create_items(total):
            counter += 1
            
            self.notify_counter.emit(counter)
            self.notify_item.emit(item)

            if counter == x:
                x += a
                sleep(0.5)

            if not self.keep_running:
                break

            self.processEvents()
 
    def stop(self):
        self.keep_running = False
  
class MyCustomDialog(QDialog):

    finish = pyqtSignal()

    def __init__(self, parent=None):
        QDialog.__init__(self, parent)

        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 = QVBoxLayout(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.init_PushButton()

    def init_PushButton(self):
        self.pushButton_start.clicked.connect(self.on_start)
        self.pushButton_stopp.clicked.connect(self.on_finish)
        self.pushButton_close.clicked.connect(self.on_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(int(i)))

    def clear_widget(self):
        list_widgets = [self.label,
                       self.tree]

        for widget in list_widgets:
            widget.clear()
            
    def on_start(self):
        self.clear_widget()
        
        task = TaskWithSignalsAndSlots()
        task.notify_counter.connect(self.on_label)
        task.notify_item.connect(self.fill_tree_widget)

        self.finish.connect(task.stop)
        
        timer = QTimer()
        timer.setSingleShot(False)
        timer.timeout.connect(task.stop)
        timer.start()
        task.start()

    def on_finish(self):
        self.finish.emit()

    def on_close(self):
        self.close()

    def closeEvent(self, event):
        self.on_finish()
    
def main():
    app = QApplication(sys.argv)
    window = MyCustomDialog()
    window.resize(600, 400)
    window.show()
    sys.exit(app.exec_())
 
if __name__ == "__main__":
    main()
Ist das so, wie du es dir vorgestellt hast, jerch? Zunächst einmal habe ich alle »QCoreApplication()«-Klassen in »QApplication()« umgeschrieben (Zeile 31 und 57). Ich hoffe einfach, dass ich an dieser Stelle richtig gehandelt habe. Meine Intention: Da ich in meinem Beispiel die »QApplication()«-Klasse verwende (Zeile 173), so verwende ich diese weiterhin.

Allerdings habe ich so meine Zweifel, dass ich dich irgendwie richtig verstanden habe. Denn die komplette Arbeit wurde in diesem Fall wieder in die »run()«-Methode der »TaskWithSignalsAndSlots()«-Klasse verschoben (Zeile 75-97). Ich weiß einfach nicht wohin mit dieser Arbeitsaufgabe. Irgendwo muss es ja ausgeführt werden.

Nun folgen ein paar Fragen: Du hast eine »TaskThread()«-Klasse erstellt, die von »QThread()«-Klasse erbt (Zeile 13). Du hast diese Klasse überschrieben, damit die »run()«-Methode einzelnes Task-Objekte zu initialisieren und auszuführen (Zeilen 25 - 31). Ich frage mal ganz naiv: war das notwendig? Heißt das jetzt, dass die »QThread()«-Klasse ansonsten mehrere Objekte initialisiert und ausgeführt hätte? Welchen Vorteil habe ich jetzt daraus gewonnen?

Als nächstest hast du die »Task()«-Klassen definiert, die von der »QObject()«-Klassen erbt (Zeile 41). Neben den Tatsachen, dass du insgesamt drei Methoden (»processEvents()« (Zeile 56), »start()« (Zeile 59) und »_finished()«(Zeile 62)) definiert hast, wird hier objektweit die »TaskThread()«-Klasse erzeugt (Zeile 53). Anschließend wird dann die »finished()-Methode der »TaskThread()«-Klasse mit der »_finished()«-Methode der »Taks()«-Klasse verbunden (Zeile 54). Die eigentliche Arbeit findet »TaskWithSignalsAndSlots()«-Klasse, die von der »Task()«-Klasse erbt, statt (Zeile 65). Soweit ich das nachvollziehen kann, findet hier die direkte Interaktion zwischen dem Programm und dem Benutzer über die GUI statt.

Jetzt konnte ich zwar einzelne Elemente aufzählen, aber irgendwie habe ich die ganze Dimension noch nicht verstanden. Ich komme mir vor, wie jemand, der in einem Wort alle Buchstaben benennen kann, aber nicht darauf kommt, was das nun für ein Wort ist, und welche Bedeutung dahintersteckt.

Des Weiteren habe ich deine Anmerkung zu der »moveToThread()«-Methode der »WorkObject()«- Klasse nicht ganz verstanden. Was genau und konkret war das falsch? Liegt es daran, weil ich in meinem vorherigen Beispiel die »WorkObject()«- Klasse dialogweit erzeugt und diese dann auf lokaler Ebene in der »on_start()«- Methode der »MyCustomDialog()«- Klasse in die »QThread()«- Klasse verschoben habe? Wieso ist dieser Vorgang mehr als ungünstig?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Naja, ich hatte den Code jetzt nicht als Vorlage zur Lösung Deines Problems gedacht, aber das geht natürlich damit. Wie Du richtig bemerkt hast, arbeitet die Task-Klasse wiederum mit der `run`-Methode und zusätzlich mit dem händischen Aufruf des Eventdisptachers (dieses `processEvents`), was die Umformung des Problems in eine ereignisgesteuerte Variante erspart. Ich denke allerdings, dass Du wenigstens die Umformung mal gemacht haben solltest, um selbst abschätzen zu können, was wann besser geeignet ist.

Zu allem anderen antworte ich mal im Text:
Sophus hat geschrieben:... Ist das so, wie du es dir vorgestellt hast, jerch? Zunächst einmal habe ich alle »QCoreApplication()«-Klassen in »QApplication()« umgeschrieben (Zeile 31 und 57). Ich hoffe einfach, dass ich an dieser Stelle richtig gehandelt habe. Meine Intention: Da ich in meinem Beispiel die »QApplication()«-Klasse verwende (Zeile 173), so verwende ich diese weiterhin.
QApplication leitet von QCoreApplication ab, daher ist die Umstellung nicht nötig und eigentlich auch nicht sinnvoll, da es für die TaskThread und Task-Klasse die Abhängigkeit zu den Qt GUI-Klassen einführt.
Sophus hat geschrieben: Allerdings habe ich so meine Zweifel, dass ich dich irgendwie richtig verstanden habe. Denn die komplette Arbeit wurde in diesem Fall wieder in die »run()«-Methode der »TaskWithSignalsAndSlots()«-Klasse verschoben (Zeile 75-97). Ich weiß einfach nicht wohin mit dieser Arbeitsaufgabe. Irgendwo muss es ja ausgeführt werden.
Genau. Versuch doch mal diese simple Zählschleife in eine ereignisgesteuerte Version mittels QTimer zu überführen, dann wird Dir das klarer:

Code: Alles auswählen

def endless_counter():
    c = 0
    while True:
        # do something
        c += 1
Sophus hat geschrieben: Nun folgen ein paar Fragen: Du hast eine »TaskThread()«-Klasse erstellt, die von »QThread()«-Klasse erbt (Zeile 13). Du hast diese Klasse überschrieben, damit die »run()«-Methode einzelnes Task-Objekte zu initialisieren und auszuführen (Zeilen 25 - 31). Ich frage mal ganz naiv: war das notwendig? Heißt das jetzt, dass die »QThread()«-Klasse ansonsten mehrere Objekte initialisiert und ausgeführt hätte? Welchen Vorteil habe ich jetzt daraus gewonnen?
Die Ableitung war nötig, um die Task-Funktionalität nach aussen hin zu kapseln, sodass man nur noch ein Task-Exemplar erstellen muss, ggf. Slots an Signale hängt und mit `.start` die Sache ohne weiteres Zutun läuft. Auch hab ich das Ding nur schnell zusammengeschrieben. So finde ich die zusätzliche `init`-Funktion unglücklich - sie versucht, ein Stück weit der Threadaffinität von QObjects entgegen zu kommen, ist aber nach wie vor simpel aushebelbar. Auch fehlt bei der Exception der Fehlertyp. Das sind Sachen, die einem auffallen, wenn man ein zweiten Mal drüberschaut.
Generell kannst Du beliebig viele Objekte in einen Thread verschieben und dort nutzen, jepp.
Sophus hat geschrieben: Als nächstest hast du die »Task()«-Klassen definiert, die von der »QObject()«-Klassen erbt (Zeile 41). Neben den Tatsachen, dass du insgesamt drei Methoden (»processEvents()« (Zeile 56), »start()« (Zeile 59) und »_finished()«(Zeile 62)) definiert hast, wird hier objektweit die »TaskThread()«-Klasse erzeugt (Zeile 53). Anschließend wird dann die »finished()-Methode der »TaskThread()«-Klasse mit der »_finished()«-Methode der »Taks()«-Klasse verbunden (Zeile 54). Die eigentliche Arbeit findet »TaskWithSignalsAndSlots()«-Klasse, die von der »Task()«-Klasse erbt, statt (Zeile 65). Soweit ich das nachvollziehen kann, findet hier die direkte Interaktion zwischen dem Programm und dem Benutzer über die GUI statt.
Das mit der GUI verstehe ich nicht, ich nutze im Bsp. nix GUI-haftes. Für eine schöne API-Spec fehlt der Task-Klasse eine virtuelle `run`-Methode als Platzhalter.
Sophus hat geschrieben: Des Weiteren habe ich deine Anmerkung zu der »moveToThread()«-Methode der »WorkObject()«- Klasse nicht ganz verstanden. Was genau und konkret war das falsch? Liegt es daran, weil ich in meinem vorherigen Beispiel die »WorkObject()«- Klasse dialogweit erzeugt und diese dann auf lokaler Ebene in der »on_start()«- Methode der »MyCustomDialog()«- Klasse in die »QThread()«- Klasse verschoben habe? Wieso ist dieser Vorgang mehr als ungünstig?
Ungünstig an dem Vorgehen ist zum einen, dass die dialogweite Verfügbarkeit suggeriert, dass dieses Objekt ein Kind des Dialoges ist und entsprechend darauf zugegriffen werden kann. Stimmt aber nicht, denn sobald das Objekt im Thread ist, ist das mit großen Einschränkung verbunden. Das `moveToThread` geht beim 2. Mal baden ausbesagten Gründen, einfach mal testen.
Antworten