Benchmark: Arbeiten im Thread

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

Sonntag 18. November 2018, 16:27

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: 6215
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sonntag 18. November 2018, 17:32

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: 1105
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Sonntag 18. November 2018, 17:42

@__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: 10358
Registriert: Sonntag 21. Oktober 2012, 17:20

Sonntag 18. November 2018, 18:11

@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: 1105
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Sonntag 18. November 2018, 18:15

@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: 6215
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sonntag 18. November 2018, 18:17

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: 1105
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Sonntag 18. November 2018, 18:26

@__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: 6215
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sonntag 18. November 2018, 18:36

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

Sonntag 18. November 2018, 18:39

@__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: 6215
Registriert: Mittwoch 14. Oktober 2015, 14:29

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.
Sirius3
User
Beiträge: 10358
Registriert: Sonntag 21. Oktober 2012, 17:20

Sonntag 18. November 2018, 19:02

@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: 1105
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Sonntag 18. November 2018, 19:14

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: 1105
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Sonntag 18. November 2018, 19:17

__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: 10358
Registriert: Sonntag 21. Oktober 2012, 17:20

Sonntag 18. November 2018, 19:43

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: 1105
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Sonntag 18. November 2018, 19:50

@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?
Antworten