QThread - Anzahl der Instanzen limitieren

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
sev
User
Beiträge: 16
Registriert: Freitag 19. Juli 2019, 22:01

Hallo zusammen,

ich habe neuerdings das erste mal QThread verwendet um eine ProgressBar zu füllen, wenn ein mouseover-event von einem Button ausgelöst wird.

Mein Problem ist nun, dass theoretisch (wenn der Nutzer seine Maus wild über die Buttons fährt), so viele Threads erstellt werden, dass das Programm abschmiert.

Ich habe versucht herauszufinden, wie ich die Anzahl an Instanzen eines QThreads limitieren kann, oder einen anderen Thread beenden kann, wenn ein neuer erstellt wird, aber leider ohne Erfolg.

Wie kann ich laufende Threads beenden oder die maximal Anzahl an parallel laufenden Threads begrenzen?

Hier ein Auszug:

Code: Alles auswählen

#Thread

class External(QThread):

#Die Anzahl an laufenden Threads begrenzen?

   countChanged = pyqtSignal(int)
   finished = pyqtSignal()

   def run(self):
      count = 0
      while count < 100:
         count += 1
         time.sleep(0.005)
         self.countChanged.emit(count)
      self.finished.emit()


#EventFitler für den mouseover Effekt

   def eventFilter(self, object, event):
      if event.type() == QEvent.Enter:
      
      #Den alten Thread hier beenden?
      
         self.calc = External()
         self.calc.start()
         self.calc.countChanged.connect(self.onCountChanged)
         self.calc.finished.connect(lambda: self.onFinish(object))
         return True
      return False

   def onCountChanged(self, value):
      self.progressBar.setValue(value)

   def onFinish(self, object):
      self.progressBar.setValue(0)

.
.
.

if __name__ == '__main__':
   app = QApplication(sys.argv)
   MainPage().show()
   sys.exit(app.exec_())
`

Danke und Gruß,
Sev
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@sev: Du könntest beispielsweise einfach keinen neuen Thread starten wenn schon einer existiert. Dazu vorher prüfen ob `self.calc` an `None` gebunden ist und bei `onFinish()` das Attribut auf `None` setzen.

Warum erstellst Du selbst ein `finished`-Signal und sendest das selbst aus? Was gefällt Dir an `QThread.finished` nicht?

Die ``while``-Schleife in `run()` ist eigentlich eine ``for``-Schleife. Und zumindest für dieses Beispiel braucht man keine(n) Thread(s), da tut's auch ein `QTimer`.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Statt alte Threads zu beenden bevor man neue startet, macht man das ganz einfach so, dass man einen (oder mehrere, wenn das denn sinnvoll ist) Threads startet, die jeweils eine Queue mit Arbeitsauftraegen haben. Das kann man explizit machen, oder wenn man die Mechanismen von Qt hier korrekt einsetzt implizit durch die sogenannten "queued connections" mit abfruehstuecken. Wenn der Thread gerade nichts zu tun haben sollte, macht das nichts - der wartet dann rum und kostet (fast) nichts.
sev
User
Beiträge: 16
Registriert: Freitag 19. Juli 2019, 22:01

Hallo blackjack,

ich hab das QThread.finished eingefügt, funktioniert super. Danke für den Hinweis.

Wie genau kann ich denn prüfen ob self.calc None ist, wenn self.calc noch gar nicht erstellt wurde?

Code: Alles auswählen

class External(QThread):
   countChanged = pyqtSignal(int)

   def run(self):
      count = 0
      while count < 100:
         count += 1
         time.sleep(0.005)
         self.countChanged.emit(count)


#EventFitler für den mouseover Effekt

   def eventFilter(self, object, event):
      if event.type() == QEvent.Enter:
           
         self.calc = External()
         self.calc.start()
         self.calc.countChanged.connect(self.onCountChanged)
         self.calc.finished.connect(lambda: self.onFinish(object))
         return True
      return False

   def onCountChanged(self, value):
      self.progressBar.setValue(value)

   def onFinish(self, object):
      self.calc = None
      self.progressBar.setValue(0)

.
.
.

if __name__ == '__main__':
   app = QApplication(sys.argv)
   MainPage().show()
   sys.exit(app.exec_())
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Um sowas pruefen zu koennen soll man eben jedes Attribut einer Klasse schon im Konstruktor anlegen. Dann muss man da nicht komisch drumrumarbeiten.

Aber nochmal: man macht das nicht so. Man legt einfach IMMER einen Thread schon am Anfang an, und wenn es was zu tun gibt, schubst man dem das rueber. Das loest dann so ein Problem von "ist der schon gestartet oder nicht?!?" gleich mit.
sev
User
Beiträge: 16
Registriert: Freitag 19. Juli 2019, 22:01

Ich hab den Thread jetzt an den Anfang gelegt.

Ein Problem habe ich nun aber noch.

Momentan muss ein Thread immer erst fertig sein, wenn ich einen neuen aufmache, sprich wenn der Nutzer schnell über Button 1 und dann auf Button 2 mit der Maus geht, wird nur der Thread von Button 1 abgearbeitet.
Wenn ich aber mehrere Threads gleichzeitig zulasse, werden die Threads von Button 1 und Button 2 gleichzeitig ausgeführt und der Wert von der ProgressBar springt immer zwischen den ausgaben der beiden laufenden Threads.

Eigentlich möchte ich in diesem Beispiel, dass der Thread von Button 1 stoppt, wenn der Nutzer auf Button zwei fährt, bevor der Thread von Button 1 fertig ist.

Mit einer queue oder ähnlichem kann ich das doch nicht erreichen, da ja alle Threads nacheinander abgearbeitet werden?
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sowas geht nicht so ohne weiteres. Du kannst Threads nicht stoppen. Du kannst bestenfalls die anfallende Arbeit in lauter kleine Haeppchen teilen, und die nacheinander abfackeln. Und dann die Haeppchen (oder den einen Happen) von Button 2 priorisiert ausfuehren. Alternativ kann die Aufgabe in Thread 1 permanent pruefen, ob ein angelegter 2ter Thread gerade etwas tut, oder nur wartet. Das ist im Grunde semantisch dann dasselbe wie kleine Haeppchen.

Aber warum soll das so sein? Warum kann das nicht nacheinander abgearbeitet werden? Warum ist Button 2 wichtiger als Button 1?
sev
User
Beiträge: 16
Registriert: Freitag 19. Juli 2019, 22:01

Da der Thread nicht anderes macht als eine (immer die selbe) ProgressBar von 0 auf 100 aufzufüllen, ist es problematisch, wenn ich zwei Threads am laufen habe, die auf die gleiche ProgressBar zugreifen.

Thread 1 gibt value = 30 weiter --> ProgressBar springt auf 30%
Thread 2 startet und gibt value = 1 weiter --> ProgressBar springt auf 1%
Thread 1 ist wieder dran und gibt value = 31 weiter --> ProgressBar springt auf 31%
Thread 2 ist dran und gibt value = 2 weiter --> ProgressBar springt auf 2%

Immer wenn der Nutzer über einen neuen Button fährt, sollte die ProgressBar eigentlich zurückgesetzt werden und sich langsam von 0 auf 100 auffüllen.
Erst wenn die ProgressBar 100 erreicht, wird eine Funktion ausgelöst, daher sollte die ProgressBar auch immer wieder zurückgesetzt werden wenn sich ein Nutzer "umentscheided".

Ich hoffe das ist verständlich :|
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das Problem ist ja nur, das du die Gesamtheit der Aufgabe falsch berechnest. Wenn du eine neue Aufgabe hinzufuegst, dann hast du zwei Moeglichkeiten:

- du fuegst einen eigenen Progressbar hinzu. macOS zB macht das, wenn der Finder mehrere Kopierarbeiten oder so erledigt. Dann entsteht darunter ein zweiter, dritter etc. Fortschrittsbalken.
- du definierst die Gesamtheit der zu leistenden Arbeit dann eben als doppelt so gross, und jeder Thread addiert einfach nur nach jedem Arbeitsschritt einen entsprechenden Wert. Nehmen wir mal an Thread 1 kopiert 20 Dateien, erledigt also immer 5% mit einem Schritt, und Thread zwei kopiert 4 Dateien, also 25% pro Schritt. Dann startet der erste Auftrag, und der Fortschrittsbalken geht von 0-100. Dann kommt der zweite hinzu, und jetzt muss der Balken von 0-200 gehen, und jeder Thread addiert jeweils seine 5 oder 25 auf die geleistete Arbeit drauf.
sev
User
Beiträge: 16
Registriert: Freitag 19. Juli 2019, 22:01

Mein Anwendungsbeispiel ist etwas anders.

Die Progress Bar stellt keinen wirklichen Arbeitsfortschritt dar, sondern soll nur eine sich aufbauende Verbindung darstellen.

Sprich wenn der Nutzer auf Button1 fährt, füllt sich eine ProgressBar die in der Mitte von zwei Bildern steht. Bild1 ist fest und Bild2 ändert sich je nachdem über welchen Button der Nutzer fährt.
Wenn der Nutzer also auf Button1 fährt, ändert sich Bild2 erst, wenn die ProgressBar 100 erreicht hat (voll ist). Wenn der Nutzer davor jedoch auf Button2 fährt, soll die ProgressBar in diesem Moment wieder von 0 beginnen.

Wenn ich das Limit der ProgressBar einfach nur auf 200 setzen würde und die Fortschritte der beiden Threads addiere, springt zwar die ProgessBar nicht mehr zwischen verschiedenen Werten, aber somit kann ich auch kein "neu aufbauen" der Verbindung darstellen.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Dann benutzt du die falsche visuelle Abstraktion fuer die Darstellung. Dann musst du eben pro Verbindung etwas darstellen, wenn's sein muss einen Progressbar - aber der kann nunmal nicht schluessig zwei Dinge die parallel vorgehen darstellen, ausser sie lassen sich eben so kombinieren wie von mir beschrieben.
sev
User
Beiträge: 16
Registriert: Freitag 19. Juli 2019, 22:01

Ok, danke für deine Hilfe.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Noch ein Nachtrag: ich halte dein Vorgehen immer noch nicht fuer ideal, aber dein Problem kommt ja eher daher, dass du direkt aus dem Thread den Progressbar aenderst. Das ist halt falsch. Du musst abhaengig vom Button eben die Bilder und den aktuellen Fortschritt darstellen. Um das dann auch noch dynamisch zu machen (also den Balken zu bewegen waehrend man ueber dem entsprechenden Button schwebt) sollte die Abfrage des Ist-Zustandes einfach mit einem Timer erfolgen. Dadurch ist es dann auch einfach, das sauber zu wechseln, denn wenn man mit dem Hover/Enter/wasauchimmer Event den Bereich wechselt, kann der Timer gar nicht zuschlagen, weil man gerade ja ein Event verarbeitet. Und dann hievt man einfach alles um. Was *nicht* geht, ist die Threads die Signale senden zu lassen und die umzuhaengen. Theoretisch geht das, aber weil das Signal wirklich asynchron durch den Thread kommt, kann es sein, das man dann glitches bekommt, weil waehrend des umkonfigurierens ein Event gekommen ist. Das will man ja nicht.
Benutzeravatar
sparrow
User
Beiträge: 4195
Registriert: Freitag 17. April 2009, 10:28

Soll der erste laufende Thread eigentlich ohne Ergebnis beendet werden, wenn der zweite gestartet wird?
sev
User
Beiträge: 16
Registriert: Freitag 19. Juli 2019, 22:01

@deets: ja ich denke du hast recht, Threads scheinen hier nicht der richtige bzw. schöne weg zu sein.

@sparrow: Ja, damit müsste man jedenfalls einen Reset simulieren können.
Antworten