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: Ich werde im Text an entsprechenden Stellen antworten.
jerch hat geschrieben: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.
Eines vorweg: Deine Vorlage sollte für mich nicht als Lösung dienen. Ich versuchte und versuche immer noch innerlich bis in die kleinste Ecke zu verstehen, was hier passiert. Es bringt mir nichts, wenn ich etwas eins zu eins übernehme. Ich möchte Python lernen und verstehen. Dazu dienen dann Vorlagen für die Analyse sehr hervorragend. Denn das in Worten geschriebene Text wirkt auf mich zu trocken und zu abstrakt. Für Profis ist das natürlich alles offensichtlich und klar nachvollziehbar. Nun habe ich eine gute Überleitung zu meiner Verwirrtheit gebracht. Du redest von Umformung in eine ereignisgesteuerte Variante. Den Satz kann ich lesen, verstehe jedes Wort. Aber was genau meinst du mit Umformung? Kannst du mir dazu bitte ein pseudo-Programm schreiben. Zum Beispiel in diesem Stil "Eigentlich würde man das so machen, aber dann würde man umformen". So das man eine Art »Vorher und Nachher«-Bild hat. Ich sitze nämlich hier und denke mir "Ähm, ja, und?"
jerch hat geschrieben: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.
Das verstehe ich nicht. Wenn die »QApplication()«-Klasse von der »QCoreApplication« ableitet, dann ist es doch Jacke wie Hose, ob ich nun die »QApplicationen()«-Klasse in meinem Beispiel-Programm verwende. Denn in meinem Mini-Projekt arbeite ich (natürlich einmal) mit der »QApplication()«-Klasse. Daher verstehe ich nun nicht, was daran weniger sinnvoll sein soll? Da bin ich leider nicht ganz mitgekommen.
jerch hat geschrieben: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
Ich möchte nicht mit der While-Schleife arbeiten, weil sie für mich wesentlich langsamer ist als die »xrange()«-Methode. Aber bei deiner Aufgabenstellung komme ich zum gleichen Ergebnis. Hier das Ergebnis:

Code: Alles auswählen

import sys

from PyQt4.QtCore import QTimer
from PyQt4.QtGui import QApplication

def create_items(total):
    for item in xrange(total):
        yield item

def output_items():
    for element in create_items(10):
        print "element", element

def main():
    app = QApplication(sys.argv)
    app.setQuitOnLastWindowClosed(False)
    create_timer()
    timer = QTimer()
    timer.timeout.connect(output_items)
    timer.start(0)

    # run event loop so python doesn't exit
    app.exec_()

if __name__ == "__main__":
    main()
In der »create_items()«-Funktion sollen später in meinem Projekt die Datensätze aus der Datenbank gelesen werden. Daher auch diesen Generator. Man will ja mit dem Arbeitsspeicher und mit den Ressourcen des Rechners sparsam umgehen. Damit der Generator überhaupt entsprechend angesprochen werden kann, nehme ich hier die »output_items()«-Funktion. So arbeitet man mit Generatoren, richtig? In der »main()«-Funktion erzeuge ich eine Instanz der »QAppliaction()«-Klasse. Anschließend erzeuge ich eine weitere Instanz der »QTimer()«-Klasse. Damit der »QTimer« bei jedem sogenannten »timeout()« etwas auslösen soll, wird die »output_items()«Funktion zum Inhalt der »timeout()«-Methode. Damit hätte ich eine Verbindung hergestellt. Und damit der »QTimer« auch sofort mit der Arbeit beginnt, wird der »start()«-Methode der »QTimer()«-Klasse einfach die Zalh Null übergeben. Und damit das Programm nicht sofort abbricht, wird auf der Instanz der »QApplication()«-Klasse die »exec_()«-Methode ausgeführt. So würde ich meine Aufgabe lösen. Du siehst. Im Grunde finde ich bei mir keinen anderen Weg. Eigentlich der Gleiche Weg wie in all meinen anderen Beispielen.
jerch hat geschrieben: 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.
Mir ist dieser Fehler nicht aufgefallen. Verwundert aber auch die wenigsten :) Ich hatte mich allerdings gefragt, wieso du die »init()«-Methode angewendet hast? Ich hätte die Variable in der »run()«-Methode auf True gesetzt und wäre damit fertig.
Sophus hat geschrieben: 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.
Mein Blick würde jetzt sagen: "Ähm, ja... virtuelle Methode... was zum Geier....?"
jerch hat geschrieben: 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.
Habe ich schon - also getestet. Würde ich die Instanzt der »WorkObject()«-Klasse lokal in der »on_start()«Methode erzeugen, so würde das Programm nicht einmal anlaufen. Es sei denn, ich setze am Ende der »on_start()«-Methode ein eine »print«-Anweisung, dann beginnt die Arbeit. Daher bin ich dazu übergegangen, und habe die Instanz der »WorkObject« dialogweit erzeugt. Eine andere und bessere Lösung fiel mir nicht ein.

Damit du nicht groß suchen musst, hier das ältere Beispiel: Nimm mal die »Print«-Anweisung in Zeile 119 weg, du wirst sehen. Keine Reaktion. Das war auch der Grund, weshalb ich die die Instanz der »WorkObject«-Klasse dialogweit erzeugt habe. Ich gebe zu, das ist keineswegs eine elegante Lösung. Nur damit du meinen Gang nachvollziehen kannst.

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 WorkObject(QObject):
 
    notify_progress = pyqtSignal(object)
    fire_label = pyqtSignal(object)
    finished = pyqtSignal()

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


        

    def add_items(self):
         total = 190000
         
         x = (total/100*1)
         a = x

         counter = 0

         for element in create_items(10000):
              counter += 1
              self.notify_progress.emit((element))
              self.fire_label.emit((counter))

              if counter == x:
                   x += a
                   sleep(1)
                   
              if not self.keep_running:
                   self.keep_running = True
                   break
 
    def run(self):
         self.keep_running = True
         self.add_items()

    def stop(self):
         self.keep_running = False
         

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)

         work_object = WorkObject()

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

         self.finish.connect(work_object.stop)

         work_object.moveToThread(task_thread)

         task_thread.started.connect(work_object.run)

         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
         timer.setSingleShot(False)
         timer.setInterval(7000)
         timer.timeout.connect(work_object.stop)
         timer.start()

         task_thread.start()

         print 

    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

Hmm wo anfangen, naja ich versuchs mal ganz basal...
Sophus hat geschrieben:Du redest von Umformung in eine ereignisgesteuerte Variante. Den Satz kann ich lesen, verstehe jedes Wort. Aber was genau meinst du mit Umformung? Kannst du mir dazu bitte ein pseudo-Programm schreiben. Zum Beispiel in diesem Stil "Eigentlich würde man das so machen, aber dann würde man umformen". So das man eine Art »Vorher und Nachher«-Bild hat. Ich sitze nämlich hier und denke mir "Ähm, ja, und?"
Nehmen wir mal die simple Zählschleife von oben und formen die um. Wichtigster Unterschied bei ereignisgesteuert ist, dass Du den Haupt-Kontrollfluss an ein Ereignissystem abgibst. Üblicherweise wird im Ereignissystem die Verarbeitung über eine Endlosschleife abgebildet, hier mal in Pseudocode:

Code: Alles auswählen

while True
    ereignis = hole_ereignis()
    # Sonderregel für Abbruch
    if ereignis == ABBRUCH
        break
    verarbeite(ereignis)
Das ist stark vereinfacht das, was `QEventLoop` macht, wenn Du `exec_()` startest und mit `quit()` wieder anhältst. Du benutzt das ja bereits in Deiner GUI-Hauptanwendung, wahrscheinlich ohne genau zu wissen, was es eigentlich macht. (Das Modell ist grob vereinfacht, weil es offen lässt, wie Ereignisse registriert und geholt werden.)

Zurück zur Endlos-Zählschleife - die soll jetzt in unserem Pseudo-System realisiert werden. Das ist relativ einfach, da wir im Ereignissystem schon eine Endlosschleife haben, die wir nutzen können und nur noch die Arbeit eines Schleifendurchlaufes an ein Ereignis hängen müssen:

Code: Alles auswählen

counter = 0  # Startzustand

Ereignis COUNTER:
    counter = counter + 1
Jetzt brauchen wir noch was, was das Ereignis COUNTER programmatisch absetzt. Dafür bieten alle Ereignissysteme etwas Entsprechendes an, z.B. `setTimeout` in Javascript oder `QTimer` in Qt.

Was der Pseudocode nicht beachtet, sind Scopingregeln der jeweiligen Sprache. Mit Python umgesetzt, wäre der Einsprungpunkt des Ereignisses COUNTER ein Funktionsaufruf, heisst man muss sicherstellen, das `counter` innerhalb der Funktion auch sichtbar und modifizerbar ist. In Python kann man das per Closure, Objekt oder Parameter sicherstellen (der Vollständigkeit halber auch per global). In PyQt könnte man es z.B. so machen:

Code: Alles auswählen

from PyQt4.QtCore import QCoreApplication, QTimer, QObject
import signal

class Counter(QObject):
    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.value = 0
    def increment(self):
        self.value += 1
        print self.value

if __name__ == '__main__':
    app = QCoreApplication([])

    # triggert ABBRUCH mit Strg+C
    signal.signal(signal.SIGINT, lambda s, f: app.quit())

    timer = QTimer()
    counter = Counter()

    # assoziiert increment() mit TIMEOUT Ereignis
    timer.timeout.connect(counter.increment)

    timer.start()

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

@jerch: Besten dank. Du leistest tolle Arbeit, und opferst dir die Zeit und Nerven, um mir das alles zu erklären. Aber ich habe das Gefühl, du gehst keineswegs auf meine Generator-Idee ein. Auch habe ich das Gefühl, dass ich mich ständig im Kreis bewege. Nun, ich habe mir mal die Freiheit genommen, und deinen Quelltext ein wenig erweitert. Hier ist das kleine Programm:

Code: Alles auswählen

from PyQt4.QtCore import QCoreApplication, QTimer, QObject
import signal
 
class Counter(QObject):
    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.value = 0

    def my_gen(self):
        
        names_list = ['Karl',
                      'Hubert',
                      'Dagebort',
                      'Heinz',
                      'Joseph',
                      'Paul']
        
        for name in names_list:
            yield name 
            
    def increment(self):
        for element in self.my_gen():
            print "Output:", element
 
if __name__ == '__main__':
    app = QCoreApplication([])
 
    # triggert ABBRUCH mit Strg+C
    signal.signal(signal.SIGINT, lambda s, f: app.quit())
 
    timer = QTimer()
    counter = Counter()
 
    # assoziiert increment() mit TIMEOUT Ereignis
    timer.timeout.connect(counter.increment)
 
    timer.start()
 
    # startet Ereignishauptschleife
    app.exec_()
Damit der Generator mir nicht nur Zahlen generiert, habe ich eine Liste mit Namen erzeugt. Die Namen wurden willkürlich gesetzt. Wir wollen simulieren, dass die »my_gen()«-Methode der »Counter()«-Klasse die Daten aus der Datenbank liest. Wir tun einfach mal so. Wie du siehst, komme ich zum selben Ergebnis. Der Generator soll mir Zeile für Zeile die Daten aus der Datenbank herausrücken und diese dann auf meinem TreeWidget ausgegeben.

Ich hatte auch noch gehofft, dass du noch einmal kurz auf die Problematik eingehst, dass meine lokal erstelle »WorkObject()«-Instanz nicht ausgeführt werden kann, ohne dass ich die »Print«-Anweisung benutze.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Bei dem Generator bin ich mir nicht sicher, ob er das tut, was Du wolltest. `increment` geht derzeit immer alle Elemente des Generators durch. Falls Du mit `increment` immer nur das nächste Element des Generators haben willst, müsstest Du hier mit `.next()` arbeiten, da Qt keine Ahnung von Pythongeneratoren hat.

Die Sache mit der Print-Anweisung:
Das `print` schindet Zeit, was dem Thread, den Du drüber erstellst, Zeit gibt, um sich fertig zu initialisieren. Ohne `print` fehlt die Zeit und der Garbage Collector von Python räumt das `work_object` ab. Abhilfe - binde das `work_object` an ein Attribut des Threadobjektes, da gehört es semantisch auch hin. Es wird dann nach Threadende mit dem `deleteLater` abgeräumt.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

jerch hat geschrieben:@Sophus:
Bei dem Generator bin ich mir nicht sicher, ob er das tut, was Du wolltest. `increment` geht derzeit immer alle Elemente des Generators durch. Falls Du mit `increment` immer nur das nächste Element des Generators haben willst, müsstest Du hier mit `.next()` arbeiten, da Qt keine Ahnung von Pythongeneratoren hat.
Ok, die »next()«-Funktion ist ein Stichwort. Denn ich möchte in der Tat jedesmal nur das nächste Element des Generators. Dies habe ich gleich versucht umzusetzen. Aber das Ergebnis ist leider nicht wie gewünscht - dazu gleich mehr. Wir sehen, dass ich in Zeile 9 einen Generator erstellt habe. In diesem Generator ist eine Liste. In Zeile 23 verwende ich in der »increment()«-Methode die »next()«-Funktion. Da die »my_gen()«-Methode einen Iterator zurückliefert, so wurde die »my_gen()«-Methode zum Inhalt der »next()«-Funktion. Und durch diese eben genannte Funktion wünsche ich mir, dass sie mir mit jedem Aufruf ein weiteres Element liefert. Was aber passiert, ist, dass immer nur das erste Element aus der Liste zurückgeliefert wird (hier wäre das Element 'Karl').

Code: Alles auswählen

from PyQt4.QtCore import QCoreApplication, QTimer, QObject
import signal
 
class Counter(QObject):
    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.value = 0

    def my_gen(self):
        
        names_list = ['Karl',
                      'Hubert',
                      'Dagebort',
                      'Heinz',
                      'Joseph',
                      'Paul']

        #iter_list = iter(names_list)
        
        for name in names_list:
            yield name 
            
    def increment(self):
        #element = 
        print "Output:", next(self.my_gen())
 
if __name__ == '__main__':
    app = QCoreApplication([])
 
    # triggert ABBRUCH mit Strg+C
    signal.signal(signal.SIGINT, lambda s, f: app.quit())
 
    timer = QTimer()
    counter = Counter()
 
    # assoziiert increment() mit TIMEOUT Ereignis
    timer.timeout.connect(counter.increment)
 
    timer.start()
 
    # startet Ereignishauptschleife
    app.exec_()
jerch hat geschrieben: Die Sache mit der Print-Anweisung:
Das `print` schindet Zeit, was dem Thread, den Du drüber erstellst, Zeit gibt, um sich fertig zu initialisieren. Ohne `print` fehlt die Zeit und der Garbage Collector von Python räumt das `work_object` ab. Abhilfe - binde das `work_object` an ein Attribut des Threadobjektes, da gehört es semantisch auch hin. Es wird dann nach Threadende mit dem `deleteLater` abgeräumt.
Das die »Print«-Anweisung etwas mit der Zeit zutun haben könnte, war auch mein Verdacht. Allerdings traue ich mir selbst wenig zu, daher habe ich mich auch an dich gewendet. Hätte ja sein können, dass intern eine etwas andere Kette ausgelöst wird. Nun zum Eigentlich: Du sagtest, ich solle die Instanz der »WorkObject()«-Klasse an ein Attribut des »QThread()«-Objektes, richtig? Ich habe hier nur mal die »on_start()«-Methode der »QDialog()«-Klasse herausgenommen. Wird hier nicht bereits in Zeile 16 eine Verbindung hergestellt? Die »work_object«-Instanz wird an die »run()«-Methode der »task_thread«-Instanz gebunden. Ich ging immer davon aus, dass dies ausreiche. Aber du schriebst, ich solle an ein Attribut der »task_thread«-Instanz binden, korrekt? An welches Attribut? Mir leuchtet leider nicht ein, an welches Attribut ich binden soll.

Außerdem habe ich diese unten aufgeführte Methode ein wenig aufgeräumt. Ich habe die »work_object«-Instanz, sobald sie fertig ist (»finished«) mit der »quit()«-Methode der »task_thread«-Instanz verbunden. Dadurch soll dann der Thread auch beendet werden. Damit gleich nach dem Ende der »task_thread«-Instanz aufgeräumt werden soll, bin ich dazu übergegangen, und habe in Zeile 18 das Ende dieser Instanz (»finished«) mit der »deleteLater()«-Methode der gleichen Instanz verbunden. Sprich, wenn die »task_thread«-Instanz beendet wurde, soll sie dann im Anschluss mit der »deleteLater()«-Methode verbunden werden, damit die Aufräumarbeit beginnen kann.

Code: Alles auswählen

    def on_start(self):
         self.tree.clear()
         self.label.clear()
         
         task_thread = QThread(self)
         work_object = WorkObject()

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

         self.finish.connect(work_object.stop)

         work_object.moveToThread(task_thread)

         task_thread.started.connect(work_object.run)

         task_thread.finished.connect(task_thread.deleteLater)

         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
         timer.setSingleShot(False)
         #timer.setInterval(7000)
         timer.timeout.connect(work_object.stop)
         timer.start(0)

         task_thread.start()

         print
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Einen Generator erhältst Du, in dem Du eine Generatorfunktion aufrufst. Mit `next()` bekommst Du das nächste Element, bei einem neuen Generator ist das logischerweise das erste. Genau das hast Du im Code geschrieben. Du musst den Generator vor Benutzung von `increment` erstellen, und beim `next` selbst noch die StopIteration-Exception abfangen. Mit Letzterem würde ich an ein Signal absetzen, was den Timer stoppt, sonst läuft der feuchtfröhlich weiter in die Exception rein.

Mit Binden an ein Atrribut meine ich, einfach an einen Namen hängen, z.B.:

Code: Alles auswählen

...
task_thread = QThread(self)
task_thread.work = WorkObject()
Auch sollte bei der Threadlösung der Timer in den Subthread, sonst läuft dieser selbst im Hauptthread und dessen Timout-Signal muss jedesmal durch das aufwändigere Eventdispatching von Hauptthread zu Subthread durch. Und wenn Du dann noch die Threadlösung mit der Generatorsache kombinierst, bist Du bei der ereignisgesteuerten Variante angekommen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Vielen dank. Ich lerne echt sehr viel :)

Ich habe mal versucht all deine Stichwörter und Anregungen zu berücksichtigen. Die »self.timer«-Instanz habe ich in Zeile 15 erzeugt. Also im Subthread (»Counter()«). Wie du mir gesagt hast, habe ich in Zeile 13 den »self.element«-Generator erzeugt, ehe der Generator benutzt wird. Ich gebe zu, die Namensgebungen sind nicht getroffen. Aber ich denke, dies können wir mal für einen kleinen Augenblick vernachlässigen :) In Zele 4 habe ich eine lose Funktion erstellt, wohin alle Nachrichten kommen. Des Weiteren wird das Programm beendet, sobald der Durchlauf wertig ist. Das heißt, dadurch habe ich einen Grund mit der »pyqtSignal«-Methode zu arbeiten.

Eines jedoch konnte ich nicht berücksichtigen. Ich habe den Timer direkt in der StopIteration-Exception gestoppt - ohne ein Signal darauf abzusetzen. Schließlich ist der Timer in unmittelbarer Nähe.

Aber vermutlich liege ich gänzlich falsch, und du schüttelst mit dem Kopf. Ich bitte dann um etwas Nachsicht :)

Code: Alles auswählen

from PyQt4.QtCore import QCoreApplication, QTimer, QObject, pyqtSignal

def output(text):
    print "Output:",  text
 
class Counter(QObject):
    notify_progress = pyqtSignal(str)
    finish_progress = pyqtSignal()
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

        self.element = self.my_gen()

        self.timer = QTimer()

        # assoziiert increment() mit TIMEOUT Ereignis
        self.timer.timeout.connect(self.increment)
        self.timer.start()
 
    def my_gen(self):
       
        names_list = ['Karl',
                      'Hubert',
                      'Dagebort',
                      'Heinz',
                      'Joseph',
                      'Paul']
        
        for name in names_list:
            yield name
           
    def increment(self):

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

        except StopIteration:

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

            self.timer.stop()

 
if __name__ == '__main__':
    app = QCoreApplication([])
 
    counter = Counter()
    counter.notify_progress.connect(output)
    counter.finish_progress.connect(app.quit)

    # startet Ereignishauptschleife
    app.exec_()
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus: Ja super, das passt so. Zwei Dinge kannst Du an dem Bsp. erkennen - einmal Kapselung. Counter hat nach aussen nur zwei Signale. Ansonsten ist die Klasse self-contained, also enthält ihre Logik selbst und kümmert sich auch selbst um die Daten (ist ja nicht viel aber es geht eher ums Prinzip). Ich weise Dich explizit auf den Sachverhalt hin, weil ich unsere Diskussionen um sinnvolle Programmaufteilung und "wer ist wessen Kind" von vor Monaten noch in Erinnerung habe. Counter wäre hier ein Beispiel für gute Kapselung.
Die zweite Sache - jetzt hast Du "Multithreading für Arme", also ohne Threads Nebenläufigkeit von Aufgaben geschaffen. Oftmals braucht man nämlich gar keinen zusätzlichen Thread, weil die Arbeit sich gut zerlegen lässt und im Hauptthread machbar ist. Javascript lässt sich übrigens nur so programmieren.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Cool, ich habe also doch was gelernt :) Bezüglich der Kapselung: Ich denke, sobald man die eigentliche Logik in ein anderes Modul auslagert, und dieses Modul bei bedarf dann in eine sogenannte View-Klasse lädt, kapselt man auch, korrekt? Denn hier wird Logik und View getrennt, und somit ist eine Kapselung auch gegeben. So denke ich zumidnest und so handhabe ich das auch. In meinen View-Klassen kommen nur PyQt-relevanten Sachen rein. Aber da du es expliziet noch einmal erwähnst: Heißt das im Umkehrschluss, dass meine Beispiele zuvor keine Kapselungen hatten? Ich meine, auch wenn ich die Arbeitsaufgaben falsch angepackt habe, fanden die Aufgaben immer in einem Subthread statt. Anfangs nur in einem QThread(), später dann im QObject(). Die einpliziete Erwähnung hat mich etwas irritiert.

Aber eines möchte ich noch nachfragen: Du sprachst (insbesondere in diesem Beitrag) öfters von ereignisorientiert. Ist mein Beispiel deshalb ereignisorientiert, weil die Aufgabe über den QTimer angetrieben wird? Im Grunde hätte das ach ein timer-Modul das gleiche getan, und dies wäre auch ereignisorientiert, oder? Ich habe zwar alles verstanden, was ich dazu unter deiner Federführung zusammengeschrieben habe, aber mir fehlt irgendwie noch der "Aha"-Effekt.

Später werde ich noch einmal mein altes Beispiel mit meinen neuen Errungenschaften vervollständigen. Wenn ich dann deinen "Segen" bekomme, habe ich für mich schon einmal eine richtige Grundlage.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Sophus hat geschrieben:@jerch: Cool, ich habe also doch was gelernt :) Bezüglich der Kapselung: Ich denke, sobald man die eigentliche Logik in ein anderes Modul auslagert, und dieses Modul bei bedarf dann in eine sogenannte View-Klasse lädt, kapselt man auch, korrekt? Denn hier wird Logik und View getrennt, und somit ist eine Kapselung auch gegeben. So denke ich zumidnest und so handhabe ich das auch. In meinen View-Klassen kommen nur PyQt-relevanten Sachen rein. Aber da du es expliziet noch einmal erwähnst: Heißt das im Umkehrschluss, dass meine Beispiele zuvor keine Kapselungen hatten? Ich meine, auch wenn ich die Arbeitsaufgaben falsch angepackt habe, fanden die Aufgaben immer in einem Subthread statt. Anfangs nur in einem QThread(), später dann im QObject(). Die einpliziete Erwähnung hat mich etwas irritiert.
Ob Du gut gekapselt hast, merkst Du daran, ob etwas wiederverwendbar ist, ohne den Code zerschnippeln zu müssen. Das kann eine reine Daten-, Logik- oder Viewkomponente sein, oder auch eine Melange von all dem (der DateiÖffnen-Dialog von Qt ist hierfür ein Bsp). Ob eine strikte Trennung von View und Logik zweckmäßig ist, hängt immer von der Problemstellung ab.
Sophus hat geschrieben: Aber eines möchte ich noch nachfragen: Du sprachst (insbesondere in diesem Beitrag) öfters von ereignisorientiert. Ist mein Beispiel deshalb ereignisorientiert, weil die Aufgabe über den QTimer angetrieben wird? Im Grunde hätte das ach ein timer-Modul das gleiche getan, und dies wäre auch ereignisorientiert, oder? Ich habe zwar alles verstanden, was ich dazu unter deiner Federführung zusammengeschrieben habe, aber mir fehlt irgendwie noch der "Aha"-Effekt.
Genau, Du hast die Arbeit der Schleife zerlegt und die Teilschritte werden über ein QTimer-Ereignis über die Ereignisschleife "angetrieben". Der eigentliche Vorteil ist am Bsp. noch nicht ersichtlich, fällt aber im Zusammenspiel mit weiteren Ereignissen sofort ins Auge. So kannst Du den Counter 5 oder 10mal parallel starten und diese werden scheinbar gleichzeitig abgearbeitet (deshalb "Threading für Arme"). Da Qt Benutzereingaben und die GUI selbst per Ereignisschleife verarbeitet/updatet, friert die GUI nicht mehr spürbar ein während Counter die Schleife durchläuft. Aber Obacht: Wenn ein Teilschritt zulange brauchen sollte, wird das wieder spürbar und die GUI "hakelt". Chrome gibt z.B. 60 FPS für eine flüssige Darstellung an, was bedeutet, dass eine Javascript-Teilaufgabe in 10-12 ms fertig sein sollte, damit es nicht ruckelt.
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