Benchmark: Arbeiten im Thread

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,

im Thema Verständnisfrage: Wem übergebe ich die Argumente, der Klasse oder der Methode? haben __blackjack__ und __deets__ mich darauf aufmerksam gemacht, dass man Threads einmalig erstellt und im Laufe des Programmlaufes dem Thread mit Aufträge bestückt. Daraufhin habe ich eine Art Benchmark erstellt. Hierbei geht es um zwei Varianten vom Umgang mit dem Thread. Diese werde ich nachfolgend erklären, und anschließend präsentiere ich euch den dazugehörigen Quelltext. Eine weitere Anmerkung: Die nachfolgenden Quelltexte sind ausführbar.

In der ersten Variante wird jedesmal, wenn man auf den QPushButton namens "Start" klickt, ein Thread mit seinem Overhead erstellt. Das heißt, jeder Klick ein neuer Trhead. In der Counter()-Klasse wird durch die range()-Funktion simuliert, dass 10.000 Daten in das QTreeWidget() populiert werden. Die Zeit bei 10.000 Daten beträgt bei mir rund Duration: 0:00:10.353000.

Code: Alles auswählen

import sys

import time
from datetime import datetime

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):

        # start to stopp the time
        self.start_time = datetime.now()
        
        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(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()

            # end with to stopp the time
            self.end_time = datetime.now()

            # Show how much time has been elapsed.
            print('Duration: {}'.format(self.end_time - self.start_time))
        
    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 thread and populate", 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()
Die zweite Variante macht das gleiche. Auch hier werden durch die range()-Funktion 10.000 Daten simulierend vom Thread in das QTreeWidget populiert. Allerdings wird der Thread einmalig erstellt und anschließend werden dem Thread im späteren Verlauf Aufträge gegeben. Für die gleiche Arbeit beträgt hier die Zeit allerdings Duration: 0:01:29.478000.

Code: Alles auswählen

import sys

import time
from datetime import datetime

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)
    fire_label = pyqtSignal(int)
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

    def init_object(self):

        # start to stopp the time
        self.start_time = datetime.now()

        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(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.timer.stop()
            # end with to stopp the time
            self.end_time = datetime.now()

            # Show how much time has been elapsed.
            print('Duration: {}'.format(self.end_time - self.start_time))
        
    def stop(self):
        self.notify_progress.emit('Stop the loop')
        
        self.timer.stop()
      
class MyCustomDialog(QDialog):

    finish_signal = 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 thread only", self)
        self.pushButton_populate = QPushButton("Populate", 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_populate)
        layout.addWidget(self.pushButton_stopp)
        layout.addWidget(self.pushButton_close)
 
        self.pushButton_start.clicked.connect(self.on_start_thread)

        self.pushButton_populate.clicked.connect(self.on_start_populate_tree_widget)
        
        self.pushButton_stopp.clicked.connect(self.finish_signal)
        
        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_populate_tree_widget(self):
        self.task_thread.work.init_object()
       
    def on_start_thread(self):
        self.task_thread = QThread()
        self.task_thread.work = Counter(self)

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

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

        self.task_thread.finished.connect(self.task_thread.deleteLater)

        self.task_thread.start()

    def on_finish(self):
         self.finish_signal.emit()
     
def main():
    app = QApplication(sys.argv)
    window = MyCustomDialog()
    window.resize(600, 400)
    window.show()
    sys.exit(app.exec_())
 
if __name__ == "__main__":
    main()
Wie kann es möglich sein, dass die zweite Variante wesentlich langsamer ist? Beide machen die gleiche Aufgabe, und dennoch einen verhältnismäßig großen Zeitunterschied? Ich nahm an, dass gerade in der zweiten Variante die Zeit noch kürzer sei.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Also ich finde das unglaublich verwirrend. Du hast da ohne Not irgendwelche Signale die gleich heissen koennten umbenannt, so das man sich mehrere Namen merken muss wenn man probiert zu analysieren, was da eigentlich passiert. Warum hast du die denn muehselig umbenannt?

Wie dem auch sei: pyqt4 ist mir zu alt, das installiere ich jetzt nicht extra. Was aber auffaellt: im zweiten Skript benutzt du den Thread ganz anders: statt init_object im Thread aufzurufen, passiert das im Main-Thread, und bestenfalls dort werden dann 10000 Signale in den Hintergrund-Thread dispatcht. Und von dort wieder in den Main-Thread zum befuellen des Widgets. Das ist halt auch was ganz anderes, und insofern erklaert das zumindest prinzipiell den Unterschied.

Du hast denke ich immer noch nicht verstanden, was das moveToThread eigentlich wirklich fuer Konsequenzen hat, und warum man signal/slot-Connections macht.

Last but not least: dein ganzer Ansatz ist nicht wirklich sinnvoll, denn den Thread-Overhead erzeugst du im Verhaeltnis zu deiner Workload alle jubeljahre mal. Ob man nun in der Lebenszeit des Programms 1, 2, 3 oder 10mal auf den Button drueckt - das macht so gut wie kein messbaren Unterschied, ob da nun jeweils ein Thread gestartet wird oder nicht.

Das Szeniaro, um das es uns ging, war 10000 Threads (wenn wir bei deiner Fallzahl hier bleiben) zu spawnen die *einmal* etwas machen, vs. ein Thread, der 10000mal das gleiche tut. DAS kannst du ja mal probieren, und gucken, was das macht mit deinem System.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@__deets__ Du hast vollkommen Recht. Die zweite Variante ist hinfällig, da ich direkt auf den Thread zugreife, um die init_object()-Methode zu starten. Das ganze ist somit nicht threadsicher. Vor diesem Hintergrund habe ich die zweite Variante minimal überarbeitet. Hier ist der Populate-QPuschButton mit dem start_populate_signal ()-Signal aus dem Hauptthread (GUI) verbunden. Damit die Counter()-Klasse, die später threadaffin wird, über dieses Signal bescheid weißt, habe ich dieser Klasse self übergeben - Eltern-Kind-Verhältnis. In der Counter()-Klasse ist das Signal der Eltern-Klasse mit der init_object()-Methode verbunden. Aber auch hier beträgt die Zeit: Duration: 0:01:42.066000. Also weiterhin wesentlich weniger - selbst mit Signalen.

Code: Alles auswählen

import sys

import time
from datetime import datetime

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)
    fire_label = pyqtSignal(int)
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

        parent.start_populate_signal.connect(self.init_object)

    def init_object(self):

        # start to stopp the time
        self.start_time = datetime.now()

        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(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.timer.stop()
            # end with to stopp the time
            self.end_time = datetime.now()

            # Show how much time has been elapsed.
            print('Duration: {}'.format(self.end_time - self.start_time))
        
    def stop(self):
        self.notify_progress.emit('Stop the loop')
        
        self.timer.stop()
      
class MyCustomDialog(QDialog):

    finish_signal = pyqtSignal()
    start_populate_signal = 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_populate = QPushButton("Populate", 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_populate)
        layout.addWidget(self.pushButton_stopp)
        layout.addWidget(self.pushButton_close)
 
        self.pushButton_start.clicked.connect(self.on_start_thread)

        self.pushButton_populate.clicked.connect(self.start_populate_signal.emit)
        
        self.pushButton_stopp.clicked.connect(self.finish_signal)
        
        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_thread(self):
        self.task_thread = QThread(self)
        self.task_thread.work = Counter(self)

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

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

        self.task_thread.finished.connect(self.task_thread.deleteLater)

        self.task_thread.start()

    def on_finish(self):
         self.finish_signal.emit()
     
def main():
    app = QApplication(sys.argv)
    window = MyCustomDialog()
    window.resize(600, 400)
    window.show()
    sys.exit(app.exec_())
 
if __name__ == "__main__":
    main()
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: der Unterschied zwischen Deinen zwei Varianten ist nur, dass im ersten Fall ein Thread 10000 Signale an das Hauptprogramm schickt, im zweiten Fall ein Thread die 10000 Signale, die es vom Hauptprogramm bekommt, wieder ans Hauptprogramm schickt.

Weder das eine noch das andere hat mit dem zu tun, was Du vorher gemacht hast und was wir Dir hier versuchen beizubringen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Sirius3: Beziehst du dich auf die aktuelle zweite Variante, die ich nach __deets__s Anmerkung aktualisierte? Denn in der aktuellen zweiten Variante arbeite ich gezielt mit Signalen, nur diesmal umgekehrt. Sonst werden Signale vom Thread aus in Richtung Hauptthread befeuert, diesmal wird ein Signal vom Hauptthread in Richtung Thread befeuert. Und dies ist threadsicher.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Der Punkt ist, dass du hier keine wirkliche Arbeit im Thread leistest. Sondern einfach nur testest, das Qt Signale in Queues per Thread verwaltet. Und irgendwas verknurbeltest machst, so das da mal weniger signale verschickt werden. Das hat aber nichts mit dem eigentlichen Problem zu tun.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@__deets__: Ich verstehe dich nicht ganz. In diesen Beispielen, wo eigentlich nur simuliert werden soll, spiegeln sich mein wirkliches Problem wieder. Ich befolgte in dem Eingangs verlinkten Eintrag deinen Ansatz. In meinem Projekt habe ich testweise an einer Stelle genauso gemacht, wie in der aktuellen zweiten Variante. Ich erzeuge beim Programmstart zunächst einen Thread, und beim Klick auf eine Schaltfläche möchte ich meine QTreeView() befüllen. In diesem Zuge werden Datensätze aus der Datenbank (erst einmal SQLite) geholt. Es handeln sich hier um 460 Datensätze. Und mir fiel eindeutig auf, dass auf Grundlage der ersten Variante die 460 Datensätze rasch geladen werden und die QTreeView befüllt ist. Wende ich aber die Schablone der aktuellen zweiten Variante an, dann braucht es wesentlich und signifikant länger. Und in meinem Projekt verwende ich auch das Signal an, welches vom Hauptthread in Richtung QThread befeuert.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und holst du die 460 Datensaetze mit einem, oder mit 460 Threads aus den Datenbank?
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@__deets__: Mit einem Thread. Aber in der ersten Variante erzeuge ich jedesma einen Thread-Overhead, wenn ich die Datensätze laden will. Aber ich wollte eben deinen Ansatz befolgen, und erst einmal einen Thread starten, und zum späteren Verlauf auf "Load" klicken, damit die Daten geladen werden. Und wenn ich die zweite aktuelle Variante als Schablone benutze und mit dem Signal arbeite, braucht es etwas länger. Benutze ich die erste Variante als Schablone, werden die Daten schneller geladen und in die QTreeView gefüllt.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Der Punkt ist: in beiden Faellen gibt es einen Thread. Der eine koennte spaeter wiederverwendet werden, und das waere auch besser so, aber so oder so ist der Unterschied, den du beobachtest, NICHT durch den Thread-Overhead oder Mangel daran zu erklaeren, sondern du machst irgendetwas sehr komisches, das dazu fuehrt, dass du so viel laenger brauchst. Du vergleichst also Aepfel mit Birnen.
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: das machst Du eben in Deinen Beispielen nicht. Dein Thread arbeitet nichts, sondern wird über einen Timer (der einmal im Thread und einmal im Hauptprogramm läuft) mit Daten befüllt, die dann noch mehrmals per Signal hin und her geschickt werden.

Der korrekte Vergleich wäre, Du schickst beide Male direkt im Thread-Code 10000 Signale (ohne Timer) nur das eine mal erzeugst Du erst den Thread und wartest dann per Queue auf einen Befehl, beim anderen Mal gibst Du den Befehl beim Erzeugen des Threads mit. Jetzt macht das einmalige Erzeugen eines Threads keinen Unterschied mehr. Der Unterschied kommt erst, wenn Du 1000mal 10 Signale absetzen willst.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ich glaube, ich kann euch beide nicht verfolgen. Das in Thread nicht wirklich gearbeitet wird, ist mir klar. Es wird über einen QTimer eine Methode innerhalb der Counter()-Klasse angesprochen. Fortan werden simulierte Daten über xrange() vom Thread zum Hauptthread geschickt - mittels eines Signals. In der ersten Varianten wird bei jedem Klick ein kompleter Thread erzeugt, die Signale feuern ab, danach ist alles fertig. In der zweiten Variante wird weit vorher ein Thread erzeugt, und im späteren Verlauf möchte ich, dass Signale abgefeuert werden. In der Counter()-Klasse bleibt die Arbeit also gleich, nur der Vorgang auf Seiten des Hauptthreads ändert sich.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

__deets__ hat geschrieben: Sonntag 18. November 2018, 18:54 Der Punkt ist: in beiden Faellen gibt es einen Thread. Der eine koennte spaeter wiederverwendet werden, und das waere auch besser so, aber so oder so ist der Unterschied, den du beobachtest, NICHT durch den Thread-Overhead oder Mangel daran zu erklaeren, sondern du machst irgendetwas sehr komisches, das dazu fuehrt, dass du so viel laenger brauchst. Du vergleichst also Aepfel mit Birnen.
Bleiben wir bei meiner aktuellen zweiten Variante. An welcher Stelle mache ich [...] irgendetwas sehr komisches [...]?
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

Der Unterschied ist nur, aus welchem Kontext heraus `init_object` aufgerufen wird. Variante 1 im Thread via QThread-stated. Variante 2a, über ein Click-Signal im Hauptthread, Variante 2b, über ein Signal, das über ein Click-Signal angestoßen wird im Hauptthread.

Darin wird ein Timer gestartet, der in Variante 1 im Eventloop des Threads abgearbeitet wird, in Varianten 2 im Eventloop des Hauptthreads.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Sirius3: In Variante 2b muss ich ein threadsicheres Verfahren hinkriegen, damit es reibungslos klappt. Und da nun mal das Signal in einer umgekehrten Richtung verwendet wird, muss das Signal im Hauptthread emitiert/angestoßen werden, damit auf der Thread-Seite etwas ausgelöst wird. Was wäre eine Alternative, damit der QTimer weiterhin brav im eventloop vom Thread bleibt und nicht im Hauptthread?
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sehe ich nicht durch. Wirklich nicht. Edit: das bezog sich auf die Frage, was bei dir denn den Unterschied ausmacht.


Mir ist das zu viel gewurschtel. Die von mir und __blackjack__ angemerkten Vorteile eines einzelnen Worker-Threads beziehen sich darauf, dass es nichts bringt, beliebig viele Threads hochzufeuern, die sich dann einfach nur gegenseitig auf die Fuesse treten.

Hier habe ich mal ein kleines Test-Skript zusammengeworfen, das die von mir beschriebene Semantik hat. Wenn man auf einen Knopf drueckt, bekommt der wartende Hintergrund-Thread einfach seinen work-Slot aufgerufen.

Beim anderen Button wird - so wie bei dir in urspruenglichen Code den wir damals diskutiert haben - jeweils ein neuer QThread gestartet. Egal welche Methode man verwendet, es braucht beides mal auf meinem System ca. 6 Sekunden die Items in das Tree-Widgets zu stecken. Womit beide Ansaetze keinen Performance-Einbussen haben. Die ich mir auch nicht erklaeren koennte.

Warum dein Code nun so ein komisches Verhalten zeigt - ja nun. Wie gesagt, das ist mir zu schwurbelig.

Code: Alles auswählen

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
import time
import threading

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

from PyQt5.QtWidgets import (
    QApplication,
    QWidget,
    QLabel,
    QTreeWidget,
    QTreeWidgetItem,
    QPushButton,
    QVBoxLayout,
)

TASKNUMBER = 460


class Worker(QObject):
    generated_item = pyqtSignal(str)
    work_started = pyqtSignal()
    work_done = pyqtSignal()

    def work(self):
        self.work_started.emit()
        for i in range(TASKNUMBER):
            for _ in range(1000000):  # just burn some time
                pass
            self.generated_item.emit("Item {}".format(i))
        self.work_done.emit()


class TimeMeasurer(QObject):

    def start(self):
        self._start = time.monotonic()

    def stop(self):
        elapsed = time.monotonic() - self._start
        print("Time elapsed:", elapsed)


class Forwarder(QObject):
    """
    Just a holder for one signal
    """
    enqueue_work = pyqtSignal()


class BatchCounter(QObject):

    count_changed = pyqtSignal(int)

    def __init__(self):
        super().__init__()
        self._lock = threading.Lock()
        self._count = 0

    def inc(self):
        with self._lock:
            self._count += 1
        self.count_changed.emit(self._count)


    def dec(self):
        with self._lock:
            self._count -= 1
        self.count_changed.emit(self._count)


def main():
    forwarder = Forwarder()
    counter = BatchCounter()

    extra_threads = []

    def generate_work_load():
        counter.inc()
        forwarder.enqueue_work.emit()

    def generate_work_load_in_extra_thread():
        def shutdown_thread():
            print("running threads:", len(extra_threads))
            extra_thread.quit()
            extra_thread.wait()
            extra_threads.remove(extra_thread)
            print("shutdown_thread")

        extra_thread = QThread()
        worker = extra_thread.worker = Worker()
        worker.moveToThread(worker_thread)

        extra_thread.started.connect(worker.work)
        tm = extra_thread.tm = TimeMeasurer()

        worker.generated_item.connect(add_item)
        worker.work_started.connect(tm.start)
        worker.work_done.connect(tm.stop)
        worker.work_done.connect(counter.dec)
        worker.work_done.connect(shutdown_thread)
        extra_threads.append(extra_thread)
        counter.inc()
        extra_thread.start()


    app = QApplication(sys.argv)

    worker_thread = QThread()
    worker = Worker()
    worker.moveToThread(worker_thread)

    w = QWidget()
    w.setWindowTitle('ThreadTest')

    layout = QVBoxLayout(w)
    start_in_worker_thread_button = QPushButton(w)
    start_in_worker_thread_button.setText("Add Items in Worker Thread")
    start_in_worker_thread_button.clicked.connect(generate_work_load)

    start_in_extra_thread_button = QPushButton(w)
    start_in_extra_thread_button.setText("Add Items in Extra Thread")
    start_in_extra_thread_button.clicked.connect(generate_work_load_in_extra_thread)

    counting_label = QLabel(w)
    counter.count_changed.connect(lambda count: counting_label.setText("Batches: {}".format(count)))

    tree = QTreeWidget(w)

    layout.addWidget(start_in_worker_thread_button)
    layout.addWidget(start_in_extra_thread_button)
    layout.addWidget(counting_label)
    layout.addWidget(tree)

    def add_item(text):
        parent = QTreeWidgetItem(tree)
        tree.addTopLevelItem(parent)
        parent.setText(0, text)

    w.show()

    tm = TimeMeasurer()

    worker.generated_item.connect(add_item)
    worker.work_started.connect(tm.start)
    worker.work_done.connect(tm.stop)
    worker.work_done.connect(counter.dec)
    forwarder.enqueue_work.connect(worker.work)

    # the ONE background thread
    worker_thread.start()

    app.exec_()
    worker_thread.quit()
    worker_thread.wait()
    sys.exit()

if __name__ == '__main__':
    main()
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@__deets__: Erst einmal danke ich dir. Ich werde mir mal gleich deinen Beispiel-Quelltext ansehen. Aber eines vorweg. Du unterstellst mir, dass meine Beispiel-Quelltexte zu verschwurbelt seien, aber deine wirken auch nicht gerade "übersichtlicher" auf mich. Ich muss mich da auch ziemlich arg durchkämpfen - vielleicht liegt es auch daran, dass ich mich selbst noch als Anfänger betrachte. Bitte, betrachte diese Anmerkung meinerseits nicht als Angriff oder als Trotz-Reaktion.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das ist natuerlich bedauerlich. Aber gut zu wissen, dann mache ich mir in Zukunft nicht mehr die Muehe, Code zu schreiben. Spart mir UND dir Zeit!
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@deets: Es war wirklich nicht böse gemeint. Nur dachte ich mir "mhmh, zu mir sagt er, mein Quelltext sei unübersichtlich, und selbst sitze ich hier und muss mich durcharbeiten". Aber das nur so am Rande. Ich bin über deine Anmerkungen und Ratschläge dankbar - sehr sogar.

Nun, ich habe meine beiden Beispiel ein wenig überarbeitet. Das heißt, einige Namen habe ich so geändert, dass versucht habe, __deets__"Handschrift" beizubehalten. Im Prinzip sind die Beispiele identisch mit den vorherigen Beispielen - nur etwas aufgeräumter und mit hoffentlich besseren Namen versehen. Ich präsentiere euch wieder zwei Beispiele, dich sich jedoch nur minimal voneinander unterscheiden, aber die Namen aller Methoden, Signalen etc. bleiben gleich.

Sirius3 hat mich auf ein Problem aufmerksam gemacht. Ich muss irgendwie hinkriegen, dass der Timer nicht den Hauptthread landet.

Fangen wir mit der ersten Variante an: Hier wird die Arbeit beim Erzeugen des Threads via started() aufgenommen. Ganz klassisch und unproblematisch. In dieser Variante beträgt die Zeit: Duration: 0:00:46.948000

Code: Alles auswählen

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import sys
import time
from datetime import datetime

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

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

class Worker(QObject):
    
    generated_item = pyqtSignal(str)
    count_item = pyqtSignal(int)
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

        parent.start_order_signal.connect(self.start_work_timer)

        self.worker_generator = self.generate_work()

        # start to stopp the time
        self.start_time = datetime.now()

        self.work_timer = QTimer()
        self.work_timer.setSingleShot(False)
        self.work_timer.setInterval(1)
        self.work_timer.timeout.connect(self.populate_item)

    def start_work_timer(self):
        self.work_timer.start()
    
    def generate_work(self,
                      tasknumber=10000):

        count = 0
               
        for name in xrange(tasknumber):

            count += 1
            self.count_item.emit(count)
            
            yield str(name)

    def populate_item(self):

        try:
            self.generated_item.emit(next(self.worker_generator))

        except StopIteration:

            self.work_timer.stop()

            # end with to stopp the time
            self.end_time = datetime.now()

            # Show how much time has been elapsed.
            print('Duration: {}'.format(self.end_time - self.start_time))
        
class MainThread(QDialog):

    start_order_signal = pyqtSignal()

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

        self.setWindowTitle('ThreadTest')

        layout = QVBoxLayout(self)

        self.tree = QTreeWidget(self)
        self.counting_label = QLabel(self)
        
        self.create_worker_thread_button = QPushButton(self)
        self.create_worker_thread_button.setText("Create Worker Thread and start work order")
        self.create_worker_thread_button.clicked.connect(self.generate_work_load_thread)
        
        self.close_button = QPushButton(self)
        self.close_button.setText("Close")
        self.close_button.clicked.connect(self.close)
   
        layout.addWidget(self.counting_label)
        layout.addWidget(self.tree)
        layout.addWidget(self.create_worker_thread_button)
        layout.addWidget(self.close_button)

        self.generate_work_load_thread()

    def generate_work_load_thread(self):

        self.task_thread = QThread()

        self.task_thread.work = Worker(self)
        self.task_thread.work.moveToThread(self.task_thread)
        
        self.task_thread.work.generated_item.connect(self.add_item)
        self.task_thread.work.count_item.connect(self.count_population)
        
        self.task_thread.started.connect(self.task_thread.work.start_work_timer)
        self.task_thread.finished.connect(self.task_thread.deleteLater)

        self.task_thread.start()

    def count_population(self,
                         count):

        self.counting_label.setText("Item(s): {}".format(count))
    
    def add_item(self, text):
        
        parent = QTreeWidgetItem(self.tree)
        self.tree.addTopLevelItem(parent)
        parent.setText(0, text)
        
def main():

    app = QApplication(sys.argv)

    w = MainThread()
    w.show()

    app.exec_()
    sys.exit()

if __name__ == '__main__':
    main()
In der zweiten Variante wird beim Start des Programmes der Thread erzeugt, aber hier fällt die started()- Methode weg, denn wir wollen nur erst einmal den Thread erzeugen, und zu einem späteren Zeitpunkt Arbeitsaufträge senden. Damit der bereits erzeugte Thread zum späteren Zeitpunkt Aufträge annehmen kann, arbeite ich mit dem pyqtSignal(), welcher im Hauptthread liegt und von dort auch emitiert wird. Hier beträgt die Zeit: 0:01:44.222000.

Code: Alles auswählen

#!/usr/bin/env python
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import sys
import time
from datetime import datetime

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

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

class Worker(QObject):
    
    generated_item = pyqtSignal(str)
    count_item = pyqtSignal(int)
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

        parent.start_order_signal.connect(self.start_work_timer)

        self.worker_generator = self.generate_work()

        # start to stopp the time
        self.start_time = datetime.now()

        self.work_timer = QTimer()
        self.work_timer.setSingleShot(False)
        self.work_timer.setInterval(1)
        self.work_timer.timeout.connect(self.populate_item)

    def start_work_timer(self):
        self.work_timer.start()
    
    def generate_work(self,
                      tasknumber=10000):

        count = 0
               
        for name in xrange(tasknumber):

            count += 1
            self.count_item.emit(count)
            
            yield str(name)

    def populate_item(self):

        try:
            self.generated_item.emit(next(self.worker_generator))

        except StopIteration:

            self.work_timer.stop()

            # end with to stopp the time
            self.end_time = datetime.now()

            # Show how much time has been elapsed.
            print('Duration: {}'.format(self.end_time - self.start_time))
        
class MainThread(QDialog):

    start_order_signal = pyqtSignal()

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

        self.setWindowTitle('ThreadTest')

        layout = QVBoxLayout(self)

        self.tree = QTreeWidget(self)
        self.counting_label = QLabel(self)
        
        self.create_worker_thread_button = QPushButton(self)
        self.create_worker_thread_button.setText("Just create Worker Thread")
        self.create_worker_thread_button.clicked.connect(self.generate_work_load_thread)

        self.work_order_in_worker_thread_button = QPushButton(self)
        self.work_order_in_worker_thread_button.setText("Start work order")
        self.work_order_in_worker_thread_button.clicked.connect(self.start_order_signal.emit)
        
        self.close_button = QPushButton(self)
        self.close_button.setText("Close")
        self.close_button.clicked.connect(self.close)
   
        layout.addWidget(self.counting_label)
        layout.addWidget(self.tree)
        layout.addWidget(self.create_worker_thread_button)
        layout.addWidget(self.work_order_in_worker_thread_button)
        layout.addWidget(self.close_button)

    def generate_work_load_thread(self):

        self.task_thread = QThread()

        self.task_thread.work = Worker(self)
        self.task_thread.work.moveToThread(self.task_thread)
        
        self.task_thread.work.generated_item.connect(self.add_item)
        self.task_thread.work.count_item.connect(self.count_population)

        self.task_thread.finished.connect(self.task_thread.deleteLater)

        self.task_thread.start()

    def count_population(self,
                         count):

        self.counting_label.setText("Item(s): {}".format(count))
    
    def add_item(self, text):
        
        parent = QTreeWidgetItem(self.tree)
        self.tree.addTopLevelItem(parent)
        parent.setText(0, text)
        
def main():

    app = QApplication(sys.argv)

    w = MainThread()
    w.show()

    app.exec_()
    sys.exit()

if __name__ == '__main__':
    main()

Jetzt müsste ich mir eine Lösung einfallen lassen, wie ich einem bereits erstellen Thread zum späteren Zeitpunkt mit Aufträge bestücke, so dass der Timer nicht im Hauptthread liegt, sondern weiterhin im Thread bleibt. Selbst mit pyqtSignal() komme ich nicht weiter.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sophus hat geschrieben: Montag 19. November 2018, 15:09 @deets: Es war wirklich nicht böse gemeint. Nur dachte ich mir "mhmh, zu mir sagt er, mein Quelltext sei unübersichtlich, und selbst sitze ich hier und muss mich durcharbeiten". Aber das nur so am Rande.
Selbstverstaendlich war es "boese" gemeint. Du sagst es doch im naechsten Satz: es ist eine Retourkutsche. Insbesondere da es sonst keine inhaltliche Auseinandersetzung mit meinem Post gab. Nach der Retourkutsche zu schreiben, es sei aber keine solche gewesen, aendert daran nichts.
Jetzt müsste ich mir eine Lösung einfallen lassen, wie ich einem bereits erstellen Thread zum späteren Zeitpunkt mit Aufträge bestücke, so dass der Timer nicht im Hauptthread liegt, sondern weiterhin im Thread bleibt. Selbst mit pyqtSignal() komme ich nicht weiter.
Genau das zeige ich in meinem zu komplizierten Code....
Antworten