PyQt5 GUI hängt sich auf nachdem die Funktion oder der Thread abgeschlossen wurden

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.
Marvin93
User
Beiträge: 36
Registriert: Samstag 4. Mai 2019, 15:16

Mittwoch 10. Februar 2021, 03:28

Hallo,

ich habe eine kleine GUI geschrieben die verschiedene Machine Learning Modelle trainiert. Damit die GUI auch während des Trainings ansprechbar ist, verwende ich QThread. Soweit funktioniert alles perfekt. Das Training des ML Modells funktioniert, der Fortschrittsbalken funktioniert, die GUI ist auch weiterhin ansprechbar usw. Das einzige Problem ist, dass der Thread oder die Funktion irgendwie nicht richtig abgeschlossen wird.

Ich will hier nicht den gesamten Code posten. Der ist insgesamt etwas lang. Deswegen hier nur der relevanten Teil. Insgesamt hat die Klasse MainWindow noch deutlich deutlich mehr Methoden. Hier wird ein Neuronales Netz trainiert. Um genau zu sein sogar eine ganze Reihe Neuronaler Netze, weil es um eine Hyperparameteroptimierung geht. Jedenfalls werden alle NN bis zum Ende trainiert und dann hängt sich die GUI auf. Die Ergebnisse werden auch nicht auf der GUI angezeigt.

Code: Alles auswählen

class MainWindow:
    def __init__(self):
        self.ui = uic.loadUi("gui.ui")
        self.ui.train_auto_nn.clicked.connect(lambda: self.start_training_thread(self.button_auto_neural_network))

    def button_auto_neural_network(self):
        if self.check_file():
            try:
                self.disable_tabs()
                scaler = self.ui.scaler_auto_nn.currentText()
                learning_rate = json.loads(self.ui.lr_auto_nn.text())
                number_layers = json.loads(self.ui.number_of_hidden_layers_auto_nn.text())
                dropout = json.loads(self.ui.dropout_auto_nn.text())
                number_neurons = json.loads(self.ui.neurons_auto_nn.text())
                activations = json.loads(self.ui.activation_auto_nn.text().replace('\'', '"'))
                optimizer = json.loads(self.ui.optimizer_auto_nn.text().replace('\'', '"'))
                batch_size = json.loads(self.ui.batch_size_auto_nn.text())
                epochs = self.ui.epochs_auto_nn.value()
                iterations = self.ui.iterations_auto_nn.value()

                splits = self.ui.splits_auto_nn.value()
                repeats = self.ui.repeats_auto_nn.value()
                self.bool_kfold = self.ui.bool_kfold_auto_nn.isChecked()

                max_combinations = (len(learning_rate) * len(number_layers) * len(dropout) * len(number_neurons) * len(activations) * len(optimizer) * len(batch_size))

                params = {"lr": learning_rate, 'batch_size': batch_size, 'number_layers': number_layers, 'dropout': dropout, 
                                'number_neurons': number_neurons, 'activations': activations, 'optimizer': optimizer, "max_combinations": max_combinations}

                self.model = Neural_Network(features = self.dataset.get_features(), label = self.dataset.get_labels(), scaler = scaler, bool_kfold = self.bool_kfold, 
                                            splits = splits, repeats = repeats, modeltype = Neural_Network, params = params, epochs = epochs)

                accuracy, self.params = self.model.parameter_search(iterations, self.update_prograss_bar)
                print("test")
                self.enable_tabs()
                self.display_model_results(self.ui.results_auto_nn, accuracy)

    def start_training_thread(self, function):
        self.worker = WorkerThread(function) self.worker.start()                                 
        self.worker.finished.connect(self.finish_training_thread)

    def finish_training_thread(self):         
        QMessageBox.information(self, "Done!", "Worker Thread completed")

class WorkerThread(QThread):
    def init(self, function, *args, **kwargs): 
        super().init(*args, **kwargs) 
        self.function = function 
    def run(self): 
        self.function()

Der folgende Error wird mir angezeigt:

Code: Alles auswählen

QObject: Cannot create children for a parent that is in a different thread.
(Parent is QTextDocument(0x2316d8786c0), parent's thread is QThread(0x2315f43caa0), current thread is WorkerThread(0x23172a30e30)
forrtl: error (200): program aborting due to control-C event
Image              PC                Routine            Line        Source
libifcoremd.dll    00007FFD14DC3B58  Unknown               Unknown  Unknown
KERNELBASE.dll     00007FFD6255B443  Unknown               Unknown  Unknown
KERNEL32.DLL       00007FFD62AE7034  Unknown               Unknown  Unknown
ntdll.dll          00007FFD64A5D0D1  Unknown               Unknown  Unknown
QObject::~QObject: Timers cannot be stopped from another thread

Weiß jemand woran genau das liegt und wie ich den Fehler beheben kann?
Thants
User
Beiträge: 32
Registriert: Dienstag 1. Dezember 2020, 12:00

Mittwoch 10. Februar 2021, 06:55

Die Fehlermeldung enthält ja bereits den Kern der Sache, du darfst GUI-Funktionalität nicht aus mehreren Threads heraus benutzen. Stattdessen musst du dein Programm so strukturieren, dass alles was GUI betrifft im gleichen (Haupt)Thread erledigt wird und die anderen Threads wirklich nur Arbeit unabhängig von der GUI erledigen. Wenn die Threads irgendwas anzeigen wollen, müssen sie das dem Hauptthread irgendwie mitteilen (z.B. per Signal oder Event) und der erledigt dann die eigentliche Anzeige.
Benutzeravatar
__blackjack__
User
Beiträge: 8550
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Mittwoch 10. Februar 2021, 11:08

@Marvin93: Das Ende-Signal sollte man unbedingt verbinden *bevor* man den Thread startet. So kann es passieren das man den Thread startet und der beendet ist bevor das Signal verbunden wurde und dann wird das natürlich nicht mehr ausgelöst.
“Dawn, n.: The time when men of reason go to bed.” — Ambrose Bierce, “The Devil's Dictionary”
Marvin93
User
Beiträge: 36
Registriert: Samstag 4. Mai 2019, 15:16

Mittwoch 10. Februar 2021, 13:55

Thants hat geschrieben:
Mittwoch 10. Februar 2021, 06:55
Die Fehlermeldung enthält ja bereits den Kern der Sache, du darfst GUI-Funktionalität nicht aus mehreren Threads heraus benutzen. Stattdessen musst du dein Programm so strukturieren, dass alles was GUI betrifft im gleichen (Haupt)Thread erledigt wird und die anderen Threads wirklich nur Arbeit unabhängig von der GUI erledigen. Wenn die Threads irgendwas anzeigen wollen, müssen sie das dem Hauptthread irgendwie mitteilen (z.B. per Signal oder Event) und der erledigt dann die eigentliche Anzeige.
Okay, also wenn ich das richtig verstehe, dann darf ich die Funktion "button_auto_neural_network" aus der GUI Klasse nicht übergeben so wie ich es gemacht habe? Sondern muss diese quasi komplett in der run Methode der Klasse WorkerThread definieren und dann halt nur die Argumente übergeben die ich dafür brauche? Weil sich sonst die Threads quasi vermischen. Also auf der GUI hole ich mir zum Beispiel die Learning Rate (learning_rate = json.loads(self.ui.lr_auto_nn.text())) und übergebe diese dann an die Klasse WorkerThread. in der Run Methode der Klasse WorkerThread erstelle ich dann eine Instanz des Modells und trainiere es.

Code: Alles auswählen

self.model = Neural_Network(features = self.dataset.get_features(), label = self.dataset.get_labels(), scaler = scaler, bool_kfold = self.bool_kfold, splits = splits, repeats = repeats, modeltype = Neural_Network, params = params, epochs = epochs)

accuracy, self.params = self.model.parameter_search(iterations, self.update_prograss_bar)
Und die Accuracy gebe ich dann zurück. Aber nicht mit einem einfachen return wie gewohnt, sondern mit einem Signal oder Event? Das muss ich mir nochmal anschauen wie das funktioniert.
Marvin93
User
Beiträge: 36
Registriert: Samstag 4. Mai 2019, 15:16

Mittwoch 10. Februar 2021, 13:57

__blackjack__ hat geschrieben:
Mittwoch 10. Februar 2021, 11:08
@Marvin93: Das Ende-Signal sollte man unbedingt verbinden *bevor* man den Thread startet. So kann es passieren das man den Thread startet und der beendet ist bevor das Signal verbunden wurde und dann wird das natürlich nicht mehr ausgelöst.
Damit meinst du diese Zeile?

Code: Alles auswählen

self.worker.finished.connect(self.finish_training_thread)
Ich habe bisher noch nicht mit Threads gearbeitet. Diese also einfach vor diese Zeile kopieren?

Code: Alles auswählen

self.worker.start()
Benutzeravatar
__blackjack__
User
Beiträge: 8550
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Mittwoch 10. Februar 2021, 16:23

Naja nicht kopieren, verschieben.
“Dawn, n.: The time when men of reason go to bed.” — Ambrose Bierce, “The Devil's Dictionary”
Marvin93
User
Beiträge: 36
Registriert: Samstag 4. Mai 2019, 15:16

Mittwoch 10. Februar 2021, 17:03

__blackjack__ hat geschrieben:
Mittwoch 10. Februar 2021, 16:23
Naja nicht kopieren, verschieben.
Ja, klar verschieben. Kopieren im Sinne von Strg + C ohne groß drüber nachzudenken und irgendwas anzupassen. Habe mich falsch ausgedrückt :D
Benutzeravatar
__blackjack__
User
Beiträge: 8550
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Mittwoch 10. Februar 2021, 18:04

Nee eher ”kopieren” im Sinne von <Strg+X>. Oder schauen was der Editor so an Verschiebeoperationen bietet. Meiner verschiebt beispielsweise die aktuelle Zeile oder die markierten Zeilen mit <Strg+Shift+Cursor hoch/runter> nach oben oder unten. Das ist praktisch um Code ein bisschen umzuordnen und es rührt auch die Zwischenablage nicht an.
“Dawn, n.: The time when men of reason go to bed.” — Ambrose Bierce, “The Devil's Dictionary”
Marvin93
User
Beiträge: 36
Registriert: Samstag 4. Mai 2019, 15:16

Donnerstag 11. Februar 2021, 15:42

Ich habe jetzt mal versucht das ganze anzupassen. Es funktioniert aber einfach nicht.

Code: Alles auswählen

    def button_auto_neural_network(self):
        if self.check_file():
            try:
                self.disable_tabs()
                scaler = self.ui.scaler_auto_nn.currentText()
                learning_rate = json.loads(self.ui.lr_auto_nn.text())
                number_layers = json.loads(self.ui.number_of_hidden_layers_auto_nn.text())
                dropout = json.loads(self.ui.dropout_auto_nn.text())
                number_neurons = json.loads(self.ui.neurons_auto_nn.text())
                activations = json.loads(self.ui.activation_auto_nn.text().replace('\'', '"'))
                optimizer = json.loads(self.ui.optimizer_auto_nn.text().replace('\'', '"'))
                batch_size = json.loads(self.ui.batch_size_auto_nn.text())
                epochs = self.ui.epochs_auto_nn.value()
                iterations = self.ui.iterations_auto_nn.value()

                splits = self.ui.splits_auto_nn.value()
                repeats = self.ui.repeats_auto_nn.value()
                self.bool_kfold = self.ui.bool_kfold_auto_nn.isChecked()

                max_combinations = (len(learning_rate) * len(number_layers) * len(dropout) * len(number_neurons) * len(activations) * len(optimizer) * len(batch_size))

                self.params = {"lr": learning_rate, 'batch_size': batch_size, 'number_layers': number_layers, 'dropout': dropout, 
                                'number_neurons': number_neurons, 'activations': activations, 'optimizer': optimizer, "max_combinations": max_combinations}

                self.model = Neural_Network(features = self.dataset.get_features(), label = self.dataset.get_labels(), scaler = scaler, bool_kfold = self.bool_kfold, 
                                            splits = splits, repeats = repeats, modeltype = Neural_Network, params = self.params, iterations = iterations, epochs = epochs)

                self.worker = WorkerThread(self.model)
                self.worker.finished.connect(self.finish_training_thread)
                #self.accuracy = 0
                self.worker.accuracy.connect(lambda self=self: setattr(self, 'accuracy', None))
                self.worker.params.connect(lambda: self.params)
                #self.worker.worker_complete.connect((accuracy, self.params))
                self.worker.start()

                #accuracy, self.params = self.model.parameter_search(iterations, self.update_prograss_bar)
                self.enable_tabs()
                self.display_model_results(self.ui.results_auto_nn, self.accuracy)

            except json.decoder.JSONDecodeError:
                self.format_exception_pop_up()



class WorkerThread(QThread):
    update_progress = pyqtSignal()
    accuracy = pyqtSignal()
    params = pyqtSignal()
    def __init__(self, model, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.model = model
    def run(self): 
        self.function()
        accuracy, params = self.model.parameter_search()
        self.accuracy.emit(accuracy)
        self.params.emit(params)

Das Problem liegt bei den Signalen. Habe jetzt eine ganze Menge herangehensweisen probiert die ich irgendwo gefunden habe, aber irgendwie funktioniert nichts. Wie kann ich denn "accuracy" und "params" zurückgeben und das dann auch weiter verwenden. Ich finde fast nur Beispiele in denen das Signal mit einer Funktion verknüpft wird. Muss ich wirklich jetzt extra noch eine Funktion schreiben die einfach nur diese Rückgabewerte empfängt?

Generell gefällt mir das so auch noch nicht wirklich, selbst wenn es funktioniert. Weil die Methode "parameter_search()" nicht immer verwendet werden soll, sondern auch mal eine andere. Je nachdem was auf der GUI genau passiert. Außerdem verwendet die Methode "parameter_search()" weitere Methoden innerhalb der Klasse model. Wie kann ich denn aus einer der Methoden Werte zurückgeben? Muss ich das Signal dafür als Argument übergeben?
Benutzeravatar
DeaD_EyE
User
Beiträge: 610
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Donnerstag 11. Februar 2021, 16:08

OT: max_combinations könnte man anders schreiben, hat aber nichts mit dem Thread-Problem zu tun. Nur als Hinweis, dass es auch math.prod gibt.

Code: Alles auswählen

max_combinations = math.prod(
    map(
        len,
        (learning_rate, number_layers, dropout, number_neurons, activations, optimizer, batch_size)
    )
)
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Marvin93
User
Beiträge: 36
Registriert: Samstag 4. Mai 2019, 15:16

Donnerstag 11. Februar 2021, 21:07

DeaD_EyE hat geschrieben:
Donnerstag 11. Februar 2021, 16:08
OT: max_combinations könnte man anders schreiben, hat aber nichts mit dem Thread-Problem zu tun. Nur als Hinweis, dass es auch math.prod gibt.

Code: Alles auswählen

max_combinations = math.prod(
    map(
        len,
        (learning_rate, number_layers, dropout, number_neurons, activations, optimizer, batch_size)
    )
)

Ah, ja das sieht etwas eleganter aus. Auf jeden Fall praktisch. Danke für den Tipp.
Thants
User
Beiträge: 32
Registriert: Dienstag 1. Dezember 2020, 12:00

Freitag 12. Februar 2021, 20:48

Marvin93 hat geschrieben:
Donnerstag 11. Februar 2021, 15:42
Ich finde fast nur Beispiele in denen das Signal mit einer Funktion verknüpft wird.
"Fast" nur? Slots sind nunmal Funktionen. Das ist doch gerade der Sinn und Zweck des Signal/Slot-Mechanismus, dass man auf Ereignisse reagieren kann (indem Funktionen aufgerufen werden, die der Sender gar nicht kennen muss). Und der angenehme Nebeneffekt ist, dass man sich nicht um den Wechsel von einem in den anderen Thread kümmern muss, da man das dem Signal überlassen kann.

Was möchtest du denn mit einer Zeile wie der folgenden bezwecken?

Code: Alles auswählen

    self.worker.params.connect(lambda: self.params)  
In deinem WorkerThread (wo du übrigens self.function aufrufst, ohne dass self.function irgendwo definiert ist), definierst du die Signale accuracy und params als Signale, die keine Argument haben, aber in run() übergibst du Argumente. Das passt schonmal nicht zusammen.

Was soll dein WorkerThread eigentlich genau machen? Was muss er von außen erhalten? Was muss er wieder an die Außenwelt zurückgeben? (und zu welchem Zeitpunkt passiert das? Während der Thread läuft? Am Anfang? Am Ende?)

Die accuracy und params-Werte scheinen ja das Ergebnis der Berechnung zu sein, denn direkt danach beendet sich der Thread ja. In dem Fall brauchst du doch überhaupt keine Signale mehr, es gibt ja hier keinen Bedarf mehr Threads zu synchronisieren. Der Thread könnte lediglich noch ein Ende-Signal verschicken, damit die GUI weiß, dass das Ergebnis da ist und die GUI kann dann per normalem Methodenaufruf oder Attributzugriff das Ergebnis auslesen. Wenn das Ergebnis tatsächlich nur aus 1 oder 2 Objekten besteht, kannst du es meinetwegen auch mit dem Ende-Signal mitschicken.
Marvin93
User
Beiträge: 36
Registriert: Samstag 4. Mai 2019, 15:16

Samstag 13. Februar 2021, 01:02

Thants hat geschrieben:
Freitag 12. Februar 2021, 20:48
Marvin93 hat geschrieben:
Donnerstag 11. Februar 2021, 15:42
Ich finde fast nur Beispiele in denen das Signal mit einer Funktion verknüpft wird.
"Fast" nur? Slots sind nunmal Funktionen. Das ist doch gerade der Sinn und Zweck des Signal/Slot-Mechanismus, dass man auf Ereignisse reagieren kann (indem Funktionen aufgerufen werden, die der Sender gar nicht kennen muss). Und der angenehme Nebeneffekt ist, dass man sich nicht um den Wechsel von einem in den anderen Thread kümmern muss, da man das dem Signal überlassen kann.

Was möchtest du denn mit einer Zeile wie der folgenden bezwecken?

Code: Alles auswählen

    self.worker.params.connect(lambda: self.params)  
In deinem WorkerThread (wo du übrigens self.function aufrufst, ohne dass self.function irgendwo definiert ist), definierst du die Signale accuracy und params als Signale, die keine Argument haben, aber in run() übergibst du Argumente. Das passt schonmal nicht zusammen.

Was soll dein WorkerThread eigentlich genau machen? Was muss er von außen erhalten? Was muss er wieder an die Außenwelt zurückgeben? (und zu welchem Zeitpunkt passiert das? Während der Thread läuft? Am Anfang? Am Ende?)

Die accuracy und params-Werte scheinen ja das Ergebnis der Berechnung zu sein, denn direkt danach beendet sich der Thread ja. In dem Fall brauchst du doch überhaupt keine Signale mehr, es gibt ja hier keinen Bedarf mehr Threads zu synchronisieren. Der Thread könnte lediglich noch ein Ende-Signal verschicken, damit die GUI weiß, dass das Ergebnis da ist und die GUI kann dann per normalem Methodenaufruf oder Attributzugriff das Ergebnis auslesen. Wenn das Ergebnis tatsächlich nur aus 1 oder 2 Objekten besteht, kannst du es meinetwegen auch mit dem Ende-Signal mitschicken.

Ja eigentlich nur :D Tut mir leid, der Code den ich gepostet habe war nicht so ganz sauber. Ich hab da drin viel rumgepfuscht und einfach irgendwas probiert. Das war ein komischer Zwischenstand den ich da gepostet habe fällt mir auch gerade auf. Macht vieles nicht so richtig Sinn.

Also das ist wie gesagt das erste mal, dass ich überhaupt mit Threads in Kontakt komme. Habe das noch nicht so ganz verstanden. Das Ziel bei Accuracy und Params war tatsächlich diese Werte einfach zurückzugeben. Ich wusste aber einfach nicht wirklich wie. Weil ich ja die start methode aufrufe und nicht die run methode. Ich wollte aber auch während der Thread läuft immer eine Progress bar aktualisieren. Ich habe inzwischen herausgefunden wie das funktioniert. Also soweit funktioniert alles wie es soll.
Oder habe ich da noch irgendwie was grob falsch gemacht in dem Code? Hier fehlt wieder einiges. Ich will hier aber ungern hunderte von Zeilen Code posten. Das ist so der relevanten Teil.

Das einzige was mich jetzt sehr stört ist, dass ich einen Button auf der GUI habe der das Training des ML Modells abbrechen soll. Das habe ich über die Funktion terminate_training_thread(self) mit self.worker.terminate() gemacht. Das funktioniert soweit auch alles so wie es soll. Nur wird trotzdem immer self.worker.finished.connect(self.finish_training_thread) ausgelöst. Kann ich das irgendwie umgehen? Ich habe ja dafür ein extra pop up gemacht. Der Unterschied zwischen Thread wurde normal abgeschlossen und Thread wurde abgebrochen soll halt schon deutlich werden. Im Moment öffnet sich eine Messagebox mit dem Text "Training is completed" und direkt danach öffnet sich noch eine Messagebox mit "Training process was terminated". Gibt es da irgendwie eine Lösung wie ich in diesem Fall die finished methode unterdrücken kann?

Code: Alles auswählen

class MainWindow:
    def __init__(self):
        self.ui = uic.loadUi("gui.ui")
        
        self.ui.terminate_button.clicked.connect(self.exit_training_thread)

    def button_auto_neural_network(self):
        if self.check_file():
            try:
                self.ui.results_auto_nn.setPlainText("Training process is running")
                self.disable_tabs()
                scaler = self.ui.scaler_auto_nn.currentText()
                learning_rate = json.loads(self.ui.lr_auto_nn.text())
                number_layers = json.loads(self.ui.number_of_hidden_layers_auto_nn.text())
                dropout = json.loads(self.ui.dropout_auto_nn.text())
                number_neurons = json.loads(self.ui.neurons_auto_nn.text())
                activations = json.loads(self.ui.activation_auto_nn.text().replace('\'', '"'))
                optimizer = json.loads(self.ui.optimizer_auto_nn.text().replace('\'', '"'))
                batch_size = json.loads(self.ui.batch_size_auto_nn.text())
                epochs = self.ui.epochs_auto_nn.value()
                iterations = self.ui.iterations_auto_nn.value()

                splits = self.ui.splits_auto_nn.value()
                repeats = self.ui.repeats_auto_nn.value()
                self.bool_kfold = self.ui.bool_kfold_auto_nn.isChecked()

                max_combinations = math.prod(map(len,(learning_rate, number_layers, dropout, number_neurons, activations, optimizer, batch_size)))

                print(max_combinations)


                self.params = {"lr": learning_rate, 'batch_size': batch_size, 'number_layers': number_layers, 'dropout': dropout, 
                                'number_neurons': number_neurons, 'activations': activations, 'optimizer': optimizer, "max_combinations": max_combinations}

                self.model = Neural_Network(features = self.dataset.get_features(), label = self.dataset.get_labels(), scaler = scaler, bool_kfold = self.bool_kfold, 
                                            splits = splits, repeats = repeats, modeltype = Neural_Network, params = self.params, iterations = iterations, epochs = epochs)

                self.worker = WorkerThread(self.model)
                self.worker.finished.connect(self.finish_training_thread)
                self.worker.results.connect(self.get_params_and_accuracy)
                self.worker.update_progress.connect(self.update_prograss_bar)
                self.worker.start()
                self.output_textfield = self.ui.results_auto_nn

            except json.decoder.JSONDecodeError:
                self.format_exception_pop_up()


    def get_params_and_accuracy(self, results):
        self.model = results[0]
        accuracy = results[1]
        if results[2] != None:
            self.params = results[2]
        self.display_model_results(accuracy)
        self.enable_tabs()


    def finish_training_thread(self):
        msg = QMessageBox()
        msg.setWindowTitle("Done!")
        msg.setText("Training is completed")
        msg.exec_()

    def terminate_training_thread(self):
        self.worker.terminate()
        self.output_textfield.setPlainText("Training process was terminated")
        self.enable_tabs()
        self.update_prograss_bar(0)
        msg = QMessageBox()
        msg.setWindowTitle("Terminated!")
        msg.setText("Training process was terminated")
        msg.exec_()


class WorkerThread(QThread):
    update_progress = pyqtSignal(float)
    results = pyqtSignal(tuple)
    def __init__(self, model, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.model = model

    def run(self):
        self.update_progress.emit(0)
        if self.model.iterations != None:
            accuracy, params = self.model.parameter_search(self.update_progress)
            model_accuracy_params = (self.model, accuracy, params)
        elif self.model.iterations == None and self.model.modeltype == Neural_Network:
            accuracy = self.model.fit_nn(self.update_progress)
            model_accuracy_params = (self.model, accuracy, None)
        elif self.model.iterations == None and self.model.modeltype != Neural_Network:
            accuracy = self.model.train_model(self.update_progress)
            model_accuracy_params = (self.model, accuracy, None)
        self.results.emit(model_accuracy_params)
Benutzeravatar
__blackjack__
User
Beiträge: 8550
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Samstag 13. Februar 2021, 03:56

@Marvin93: Die Dokumentation rät ziemlich eindringlich von `Qthread.terminate()` ab. Siehe den Text ab: „Warning: This function is dangerous and its use is discouraged.“

Das killt den Thread einfach mittendrin, egal in was für einem Zustand das Dein Programm dadurch versetzt. Das kann durchaus auch der Grund dafür sein, dass danach das Programm hängen bleibt oder abstürzt wenn man auf den undefinierten Zustand zugreift. Das `threading`-Modul in der Python-Standardbibliothek bietet aus diesem Grund so etwas wie `terminate()` gar nicht erst an.

Die Bedingungen in der `run()`-Methode sind etwas redundant. Und man sieht da auch nicht so leicht, dass auch tatsächlich einer der Zweige genommen werden *muss*, so dass man vermuten könnte, dass `model_accuracy_params` auch undefiniert bleiben könnte, weil das nicht in einem ``else`` endet. Wenn man die Redundanz bei den Bedingungen raus nimmt, ist aber deutlich(er) zu sehen, dass `model_accuracy_params` auf jeden Fall definiert ist, egal wie die Belegung der Variablen in den Bedingungen aussehen:

Code: Alles auswählen

    def run(self):
        self.update_progress.emit(0)
        if self.model.iterations is not None:
            accuracy, params = self.model.parameter_search(
                self.update_progress
            )
            model_accuracy_params = (self.model, accuracy, params)
        else:
            if self.model.modeltype == Neural_Network:
                accuracy = self.model.fit_nn(self.update_progress)
                model_accuracy_params = (self.model, accuracy, None)
            else:
                accuracy = self.model.train_model(self.update_progress)
                model_accuracy_params = (self.model, accuracy, None)

        self.results.emit(model_accuracy_params)
“Dawn, n.: The time when men of reason go to bed.” — Ambrose Bierce, “The Devil's Dictionary”
Thants
User
Beiträge: 32
Registriert: Dienstag 1. Dezember 2020, 12:00

Samstag 13. Februar 2021, 06:09

Wie blackjack schon sagte, den Thread per terminate() zu beenden ist die Holzhammer-Methode. Da muss man dann genau hinschauen, ob da nicht irgendwelche Daten "kaputt" gehen, wenn man dem Programm einfach so den Teppich unter den Füßen wegzieht.

Um deine Frage mit dem fälschlicherweise ausgelösten "Done"-Dialog zu beantworten, wirf mal einen Blick in die QThread-Doku zum finished()-Signal. Das Signal wird immer dann ausgelöst, wenn der Thread beendet wird, egal ob der Thread sich selbst beendet oder ob du ihn abschießt, finished ist finished. Das ist also das falsche Signal für deinen Zweck. Du brauchst ein Signal, das nur dann ausgelöst wird, wenn der Thread sich von selbst beendet. So ein Signal hast du ja bereits, widme doch einfach dein "results"-Signal um und nenne es vielleicht auch eher "completed". Wenn das Signal ausgelöst wird, weißt du, dass der Thread komplett durchlief. Dann kannst du auch deine Methoden get_params_and_accuracy() und finish_training_thread() zusammenfassen (der Name der get_*-Methode ist sowieso etwas irreführend, es ist ja keine übliche "getter"-Methode, die einen Wert zurückgibt).

(Es könnte theoretisch immer noch passieren, dass jemand genau dann auf Abbrechen klickt, wenn der Thread gerade das results/completed-Signal auslöst. In deiner terminate_training_thread()-Methode gehst du davon aus, dass der Thread noch am Laufen ist und du gibst immer den Abbruch-Dialog aus, diese Annahme ist aber genau genommen falsch. Damit könnte es also immer noch passieren, dass du beide Dialoge angezeigt bekommst. Eine andere Alternative wäre, dass du doch beim finished-Signal bleibst, das results-Signal komplett streichst und stattdessen das Ergebnis im Objekt speicherst. Wenn du terminate() aufrufst, merkst du dir im GUI-Objekt, dass abgebrochen wurde und zeigst hier noch nicht den Dialog an. Die Bestätigung an den Benutzer wird im finished-Slot erledigt und erst dort testest du, ob du terminate() aufgerufen hast oder nicht und zeigst dann den entsprechenden Dialog an (damit hast du dann auch das Benutzerfeedback an der gleichen Stelle im Code und nicht je nach Fall in der Klasse verteilt). Falls es nicht aufgerufen wurde, holst du dir das Ergebnis vom Thread ab. Damit werden alle Entscheidungen, die den Zustand des Threads betreffen im GUI-Thread gefällt und man muss nichts weiter synchronisieren)
Antworten