Signals von überall aus erreichbar machen

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
jb_alvarado
User
Beiträge: 55
Registriert: Mittwoch 11. Juli 2018, 11:11

Hallo Allerseits,
so ganz verstehe ich das mit den Signalen leider immer noch nicht und brauche daher noch mal Hilfe.

Es ist so, dass ich gerne von meinem Main Thread aus einen Worker Thread starten möchte. und dieser WorkerThread ruft weitere Funktionen aus anderen Dateien auf. Eine Funktion die aufgerufen wird, startet z.B. ein subprocess und analysiert eine Videodatei mit ffmpeg. Jetzt hätte ich gerne von dieser Funktion aus die Möglichkeit dass sie Signale zu meinem Main Thread schicken kann, damit ich dort eine Progressbar updaten kann.

Wenn das Signal direkt aus meinem WorkerThread aus verwendet wird, geht das auch, aber wenn ich dafür eine Helferklasse einrichten möchte, die von überall aus erreichbar sein soll, funktioniert es nicht mehr.

Habe mal dieses Beispiel zusammen geschrieben:

Code: Alles auswählen

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

import sys
from time import sleep

from PySide2.QtCore import QObject, Signal, QThread
from PySide2.QtWidgets import (QApplication, QWidget, QLabel,
                               QPushButton, QVBoxLayout)


class Emitter(QObject):
    progress = Signal(str)

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

    def send(self, text):
        self.progress.emit(text)


class WorkerThread(QThread):
    # progress = Signal(str)

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

    def run(self):
        for i in range(1000):
            # self.progress.emit(str(i))
            Emitter().send(str(i))
            sleep(0.2)

    def stop(self):
        self.terminate()


def main():
    app = QApplication(sys.argv)

    worker = WorkerThread()
    emitter = Emitter()

    def run_worker():
        worker.start()

    def stop_worker():
        worker.stop()

    app.aboutToQuit.connect(stop_worker)

    w = QWidget()
    w.setWindowTitle('Signal Sender')

    layout = QVBoxLayout(w)
    label = QLabel(w)
    button = QPushButton(w)
    button.setText("Start")
    button.clicked.connect(run_worker)

    layout.addWidget(label)
    layout.addWidget(button)

    def update_label(text):
        label.setText(text)

    # worker.progress.connect(update_label)
    emitter.progress.connect(update_label)

    w.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()


Hier würde jetzt der Emitter direkt vom WorkerThread aus angesprochen werden, was jetzt so keinen Sinn macht, aber ich denke zum verdeutlichen sollte das genügen, weil dieses Beispiel so schon nicht mehr funktioniert.

Was kann ich denn machen, damit die Emitter Klasse funktioniert, und auch aus anderen Funktionen heraus (die vom WorkerThread ausgeführt würden) angesprochen werden kann?
Benutzeravatar
__blackjack__
User
Beiträge: 13080
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@jb_alvarado: Du erzeugst da ja für jeden Schleifenduchlauf ein neues `Emitter`-Objekt dessen Signal mit nichts verbunden ist. Dann ist doch klar, dass das auch nirgends empfangen wird. Du musst schon das `Emitter`-Objekt verwenden dessen Signal mit `update_label()` verbunden ist. Da das in der `main()`-Funktion erzeugt wird, musst das irgendwie in das `WorkerThread`-Objekt kommen. Zum Beispiel in dem man es beim erstellen dieses Objekts als Argument übergibt. Dann muss man es natürlich vorher erstellen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
jb_alvarado
User
Beiträge: 55
Registriert: Mittwoch 11. Juli 2018, 11:11

Autsch... Ja das leuchtet ein! Danke für den Hinweis, jetzt funktioniert das auch.
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Welchen Sinn soll das denn haben? Ein Signal direkt aus dem Thread geschickt ist genau gleich. Der emitter macht nichts einfacher, im Gegenteil.

Vor allem aber bin ich mir ziemlich sicher, das du durch Verwendung von Ableitung statt der von mir beim letzten Mal gezeigten moveToThread-Variante auch hier wieder keine queued connection zustande kommt, und dein Programm früher oder später abschmiert. Warum folgst du dem Beispiel nicht?
jb_alvarado
User
Beiträge: 55
Registriert: Mittwoch 11. Juli 2018, 11:11

__deets__ hat geschrieben: Freitag 26. April 2019, 11:22 Welchen Sinn soll das denn haben? Ein Signal direkt aus dem Thread geschickt ist genau gleich. Der emitter macht nichts einfacher, im Gegenteil.
Das hatte ich oben auch geschrieben, dass das in dem Beispiel nicht viel Sinn macht, es ging nur drum zu zeigen, dass das so nicht ging.
Vor allem aber bin ich mir ziemlich sicher, das du durch Verwendung von Ableitung statt der von mir beim letzten Mal gezeigten moveToThread-Variante auch hier wieder keine queued connection zustande kommt, und dein Programm früher oder später abschmiert. Warum folgst du dem Beispiel nicht?
Ich habe im Internet einige Beispiele gefunden, die das so ähnlich gemacht haben, wie ich auch. Habe auch dein Beispiel angeschaut, allerdings das mit dem "moveToThread" nicht verstanden. Ich dachte das sei einfach eine andere Variante einen Thread zu starten, und nicht, dass damit auch eine queued connection erstellt wird. Ist hier das moveToThread der einzige ausschlaggebende Punkt?
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Für die queued connection - ja.
jb_alvarado
User
Beiträge: 55
Registriert: Mittwoch 11. Juli 2018, 11:11

Ok hier wäre nun eine angepasste Variante:

Code: Alles auswählen

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

import sys
from time import sleep

from PySide2.QtCore import QObject, Signal, QThread
from PySide2.QtWidgets import (QApplication, QWidget, QLabel,
                               QPushButton, QVBoxLayout)


class Emitter(QObject):
    progress = Signal(str)

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

    def send(self, text):
        self.progress.emit(text)


def analyse(emitter):
    for i in range(10):
        emitter.send('Analyse Video: {}'.format(i))
        sleep(0.2)

    return 17.4, 204.1


def gen_filtergraph(emitter, lufs, blackdetect):
    print('lufs is: "{}" and blackframe is at: "{}"'.format(lufs, blackdetect))
    for i in range(10):
        emitter.send('Filter Video: {}'.format(i))
        sleep(0.2)

    return 'complex_filtergrah'


def encode(emitter, filtergraph):
    print('do something with: {}'.format(filtergraph))
    for i in range(10):
        emitter.send('Encode Video: {}'.format(i))
        sleep(0.2)


class Worker(QObject):
    def __init__(self, emitter):
        QObject.__init__(self)

        self._emitter = emitter

    def work(self):
        lufs, blackdetect = analyse(self._emitter)
        filtergraph = gen_filtergraph(self._emitter, lufs, blackdetect)
        encode(self._emitter, filtergraph)

    def stop(self):
        # TODO: do something usefull to quit nicely
        pass


def main():
    app = QApplication(sys.argv)

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

    worker_thread.started.connect(worker.work)

    def run_worker():
        worker_thread.start()

    def stop_worker():
        worker.stop()
        worker_thread.quit()
        worker_thread.wait()

    app.aboutToQuit.connect(stop_worker)

    w = QWidget()
    w.setWindowTitle('Signal Sender')

    layout = QVBoxLayout(w)
    label = QLabel(w)
    button = QPushButton(w)
    button.setText("Start")
    button.clicked.connect(run_worker)

    layout.addWidget(label)
    layout.addWidget(button)

    def update_label(text):
        label.setText(text)

    emitter.progress.connect(update_label)

    w.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()


Ich weiß - die Funktionen analyse/gen_filtergraph/encode könnte ich in diesem Beispiel immer noch in die Worker Klasse packen, aber mein original Code ist einfach zu komplex um alles in eine Klasse rein zu tun. So kann ich alles in separaten Dateien haben, und dort für sich debuggen.
Benutzeravatar
__blackjack__
User
Beiträge: 13080
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@jb_alvarado: Ich denke auch die Komplexität macht die `Emitter`-Klasse nicht sinnvoller. Die hat übrigens eine überflüssige `__init__()` hat. Kann es sein, dass Dir nicht klar ist/Du vergessen hast das in Python alles ein Objekt ist, auch Signale und sogar Methoden‽ Und das ein Slot nicht nur mit einem Signal verbunden werden kann.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
jb_alvarado
User
Beiträge: 55
Registriert: Mittwoch 11. Juli 2018, 11:11

__blackjack__ hat geschrieben: Freitag 26. April 2019, 13:55 @jb_alvarado: Ich denke auch die Komplexität macht die `Emitter`-Klasse nicht sinnvoller.
Also Ihr würdet alles in den Worker rein packen? Empfinde das als ziemlich unübersichtlich und schwerer zu debuggen. Vorteil den ich darin schon sehe ist, dass es darduch einfacher ist subprocesse vorzeitig zu schließen, wenn gewünscht.
Die hat übrigens eine überflüssige `__init__()` hat. Kann es sein, dass Dir nicht klar ist/Du vergessen hast das in Python alles ein Objekt ist, auch Signale und sogar Methoden‽
Man findet halt zig verschiedene Beispiele, wo Leute das so und so machen. Das in Python alles ein Objekt ist, war mir so nicht klar, auch nicht welche Konsequenzen daraus entstehen. Wenn du mir jetzt so sagst, dass das __init__ überflüssig ist, kann ich das schon nachvollziehen, aber komme da nicht immer selbst drauf.
Und das ein Slot nicht nur mit einem Signal verbunden werden kann.
Du meinst man könnte den Slot von verschiedenen Orten aus ansprechen? Klingt gut, aber wie könnte ich das in meinem Fall nutzen?
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und durch den ueberfluessigen Emitter hast du dir schon wieder die queued connection zerschossen. Um das nochmal ganz deutlich zu sagen: ob eine connection queued ist oder nicht haengt davon ab, zu welchem Thread die beteiligten Signal und Slot gehoeren. Sind die Threads die gleichen, ist sie NICHT gequeued. Sind sie unterschiedlich, wird sie gequeued. Und dein Emitter, genauso wie die in update_label referenzierten Objekte sind ALLE aus dem Main-Thread.

Schmeiss. Den. Emitter. Weg. Benutz. Ein. Signal. Auf. Dem. Worker. Bitte?
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nachtrag: es geht auch nicht darum "alles in den Worker reinzupacken". Der muss lediglich die Kommunikation per signal mit dem GUI-Thread erledigen. Wieviel Code der sonst enthaelt ist dir grundsaetzlich erstmal freigestellt. Das dein jetziger Ansatz mit Emitter aber uebersichlicher waere, bei einem Objekt das man ohne (negative) Konsequenzen komplett rauskuerzen koennte, das kann ich jetzt nicht erkennen.
jb_alvarado
User
Beiträge: 55
Registriert: Mittwoch 11. Juli 2018, 11:11

Ok ok. Ich lass mir ja gerne was sagen, hatte nur nicht das Problem gesehen. Wenn ihr sagt, dass das nicht klug ist, so zu machen - haue ich gerne den Emitter raus.

Also nur noch mal zum Nummer sicher gehen.

So wäre das nun richtig?:

Code: Alles auswählen

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

import sys
from time import sleep

from PySide2.QtCore import QObject, Signal, QThread
from PySide2.QtWidgets import (QApplication, QWidget, QLabel,
                               QPushButton, QVBoxLayout)


class Worker(QObject):
    progress = Signal(str)

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

        self.is_running = True

    def work(self):
        for i in range(1000):
            if self.is_running:
                self.progress.emit(str(i))
                sleep(0.2)

    def stop(self):
        self.is_running = False
        pass


def main():
    app = QApplication(sys.argv)

    worker_thread = QThread()
    worker = Worker()
    worker_thread.started.connect(worker.work)

    worker.moveToThread(worker_thread)

    def run_worker():
        worker_thread.start()

    def stop_worker():
        worker.stop()
        worker_thread.quit()
        worker_thread.wait()

    app.aboutToQuit.connect(stop_worker)

    w = QWidget()
    w.setWindowTitle('Signal Sender')

    layout = QVBoxLayout(w)
    label = QLabel(w)
    button = QPushButton(w)
    button.setText("Start")
    button.clicked.connect(run_worker)

    layout.addWidget(label)
    layout.addWidget(button)

    def update_label(text):
        label.setText(text)

    worker.progress.connect(update_label)

    w.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
Ist das so queued?
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

"Nicht klug" ist eine Sache. So wie es bis dato war ist es falsch und potentiell absturzerzeugend. Es geht also nicht um Stilpunkte, sondern um die Wurst.

Doch so wie es jetzt aussieht, sollte es passen. WICHTIG: progress (oder andere Signale des Workers die aus dem work gefeuert werden) IMMER ERST VERBINDEN NACH DEM moveToThread. Sonst ist's wieder fuer die Katz.
jb_alvarado
User
Beiträge: 55
Registriert: Mittwoch 11. Juli 2018, 11:11

Ok danke! Werde mir das merken!
Benutzeravatar
__blackjack__
User
Beiträge: 13080
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@jb_alvarado: Die Konsequenz das auch Signale bzw. Methoden auch Objekte sind, ist das Du genau wie bisher die Arbeit auf verschiedene Objekte/Funktionen verteilen kannst und denen einfach die `emit()`-Methode übergeben kannst. Du kannst also beispielsweise weiterhin eine `analyse()`-Funktion haben die so aussieht:

Code: Alles auswählen

def analyse(send):
    for i in range(10):
        send('Analyse Video: {}'.format(i))
        sleep(0.2)

    return 17.4, 204.1
Und das `send`-Argument ist dann die `emit()`-Methode von dem Signal:

Code: Alles auswählen

class Worker(QObject):
    progress = Signal(str)

    # …

    def work(self):
        result = analyze(self.progress.emit)
        # …
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
jb_alvarado
User
Beiträge: 55
Registriert: Mittwoch 11. Juli 2018, 11:11

Ah, das ist wirklich cool! Und so wie ich das sehe, wird dann "send()" auch im gleichen Thread ausgeführt wie der Worker und damit queued?!
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ja.
jb_alvarado
User
Beiträge: 55
Registriert: Mittwoch 11. Juli 2018, 11:11

Kann man eigentlich problemlos Funktionen im Worker aufrufen? (abgesehen von worker.work())

Ich würde gerne etwas in der Richtung umsetzten:

Code: Alles auswählen

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

import sys
from time import sleep

from PySide2.QtCore import QObject, Signal, QThread
from PySide2.QtWidgets import (QApplication, QWidget, QLabel,
                               QPushButton, QVBoxLayout)


class Worker(QObject):
    progress = Signal(str)

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

        self._pause = False
        self.is_running = True
        self.count = 0

    def work(self):
        while self.is_running:
            if not self._pause:
                self.progress.emit(str(self.count))
                self.count += 1
            sleep(0.2)

    def pause(self):
        self._pause = True

    def start(self):
        self._pause = False

    def quit(self):
        self.is_running = False


def main():
    app = QApplication(sys.argv)

    worker_thread = QThread()
    worker = Worker()
    worker_thread.started.connect(worker.work)

    worker.moveToThread(worker_thread)

    def run_worker():
        if button.text() == 'Start':
            worker.start()
            if not worker_thread.isRunning():
                worker_thread.start()
            button.setText("Stop")
        else:
            button.setText("Start")
            worker.pause()

    def stop_worker():
        worker.quit()
        worker_thread.quit()
        worker_thread.wait()

    app.aboutToQuit.connect(stop_worker)

    w = QWidget()
    w.setWindowTitle('Signal Sender')

    layout = QVBoxLayout(w)
    label = QLabel(w)
    button = QPushButton(w)
    button.setText("Start")
    button.clicked.connect(run_worker)

    layout.addWidget(label)
    layout.addWidget(button)

    def update_label(text):
        label.setText(text)

    worker.progress.connect(update_label)

    w.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

Ist jetzt nicht schön gelöst, ich weiß, es geht nur ums Prinzip: Also eine Möglichkeit den Worker zu pausieren. Theoretisch würde es auch gehen, ihn zu schließen und neu zu starten, aber ich dachte pausieren ist vielleicht etwas leichter.
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich verstehe nicht, was deine Frage mit dem Code zu tun hat.

Wie dem auch sei: ja man kann problemlos Funktionen aus dem Worker aufrufen.

Und statt mit so einer Frickelei löst man so etwas wie schon mal besprochen mit einer Queue von Arbeitsaufträgen. Dann wird der Thread wirklich schlafen gelegt, und dann bei bedarf wieder aufgeweckt.
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und noch ein Nachtrag: in dem von mir mal verlinkten anderen Beitrag hier verargumentieren wir, dass es nicht nötig ist, Threads zu startet und zu stoppen. Starte einfach einen zu Beginn der Anwendung, und lass ihn Aufträge abarbeiten. Und ein Auftrag kann dann “beende dich” sein, worauf dann natürlich mit join gewartet werden muss.
Antworten