GUI friert während Schleife ein: Thread hilft nicht?

Alles rund um die Programmierung mit Qt
pbooster2000
Beiträge: 10
Registriert: 25. November 2010 17:55

GUI friert während Schleife ein: Thread hilft nicht?

Beitrag von pbooster2000 »

Hallo zusammen,

ich bin ein Neuling in Qt, fange aber langsam an, es schätzen zu lernen. Leider hänge ich seit einigen Tagen an einem Problem, das ich alleine nicht gelöst bekomme. Vielleicht kann mir ja jemand mit seiner Erfahrung unter die Arme greifen.

Das Szenario:
Ich habe eine Schleife, in der ich irgendwas berechne, das etwas dauert. Wenn der Anwender auf den Startbutton drückt, die Schleife zu rechnen beginnt, friert solange die GUI ein. Knöpfe etc. können solange nicht gedrückt werden. Die Lösung lautet oft "bitte qApp->processEvents() benutzen".

Aber:
Nutze ich qApp->processEvents() so dauert die Schleifenabarbeitung bis zu 20mal länger, als ohne (im Vergleich 2 Sekunden vs. 40 Sekunden). Baue ich eine Abfrage in die Schleife ein, so dass nur alle 100.000 Schritte qApp->processEvents() aufgerufen wird, fängt wieder das Stottern und Haken der GUI an.

Mein Traum:
Der Anwender drückt einen Startbutton. Die Schleife fängt an zu rechnen, endet nach ca 2 bis 3 Sekunden (nicht erst nach 40). Gleichzeitig kann der Anwender, noch während die Schleife läuft, die GUI ganz normal weiter benutzen, ohne dass es "hakt" oder "stottert".

Mein Beispielcode:
Der Anwender drückt einen Button A um einen int-Wert "a" zu erhöhen. Zwei andere Buttons starten die Schleife zum Erhöhen eines int-Wertes "b" - einmal als Threadlösung, einmal ohne. Egal welchen B-Button man drückt, während die Schleife läuft, kann man nicht mehr auf den A-Button drücken. Erst wenn die B-Schleife fertig ist, wird das Drücken auf den A-Button verarbeitet. Das ist das, was ich nicht will.

Für Hilfe bin ich wirklich sehr sehr dankbar! :)

Mit besten Grüßen
pbooster2000

Code: Alles auswählen

#include <QThread>

#include "qt_thread.h"

int a = 0;
int b = 0;

Qt_Thread *q;

class MyThread : public QThread
{
public:
	void run();
};

void MyThread::run()
{
	b = 0;

	while (b < 1000000){
		b++;
		q->setb(b);

		//qApp->processEvents();
	}
	exec();
}



Qt_Thread::Qt_Thread(QWidget *parent)
    : QWidget(parent)
{
	ui.setupUi(this);

	q = this;

	connect(ui.pbIncA, SIGNAL(clicked()), this, SLOT(pbIncAClicked()));
	connect(ui.pbWithoutThread, SIGNAL(clicked()), this, SLOT(pbWithoutThreadClicked()));
	connect(ui.pbWithThread, SIGNAL(clicked()), this, SLOT(pbWithThreadClicked()));
}



void Qt_Thread::pbWithoutThreadClicked()
{
	b = 0;

	while (b < 1000000){
		b++;
		ui.lbb->setText( QString("%1").arg(b) );
	}
}

void Qt_Thread::pbWithThreadClicked()
{
	MyThread m;
	m.run();
}

void Qt_Thread::setb(int b)
{
	ui.lbb->setText( QString("%1").arg(b) );
}

void Qt_Thread::pbIncAClicked()
{
	a++;
	ui.lba->setText( QString("%1").arg(a) );
}


Qt_Thread::~Qt_Thread()
{

}
franzf
Beiträge: 3114
Registriert: 31. Mai 2006 11:15

Beitrag von franzf »

Lies die Doku GENAU durch, schau welche Sichtbarkeit QThread::run() hat, und überleg ob deine Verwendung überhaupt vorgesehen ist.
In den Details der Doku zu QThread steht doch genau, was zu tun ist. Die Links unter "See also" sollte man sich auch mal anschauen.
pbooster2000
Beiträge: 10
Registriert: 25. November 2010 17:55

Beitrag von pbooster2000 »

franzf hat geschrieben:Lies die Doku GENAU durch, schau welche Sichtbarkeit QThread::run() hat, und überleg ob deine Verwendung überhaupt vorgesehen ist.
In den Details der Doku zu QThread steht doch genau, was zu tun ist. Die Links unter "See also" sollte man sich auch mal anschauen.
Danke für deine Antwort! Ist run() sowieso der falsche Aufruf? Muss man start() benutzen?
franzf
Beiträge: 3114
Registriert: 31. Mai 2006 11:15

Beitrag von franzf »

pbooster2000 hat geschrieben:Danke für deine Antwort! Ist run() sowieso der falsche Aufruf? Muss man start() benutzen?
Genau so schauts aus. in start() wird run() in einem neuen Thread (jetzt nicht QThread, sondern Betriebssystem-Thread) ausgeführt.
Wenn man direkt run() aufrufen können sollte, müsste das public sein, es ist aber protected, um einen Zugriff maximal an abgeleitete Klassen zu erlauben.
pbooster2000
Beiträge: 10
Registriert: 25. November 2010 17:55

Beitrag von pbooster2000 »

franzf hat geschrieben:
pbooster2000 hat geschrieben:Danke für deine Antwort! Ist run() sowieso der falsche Aufruf? Muss man start() benutzen?
Genau so schauts aus. in start() wird run() in einem neuen Thread (jetzt nicht QThread, sondern Betriebssystem-Thread) ausgeführt.
Wenn man direkt run() aufrufen können sollte, müsste das public sein, es ist aber protected, um einen Zugriff maximal an abgeleitete Klassen zu erlauben.
Danke Dir, das hat mir schon sehr weitergeholfen. Die Methode start() ist mir gar nicht aufgefallen.

Mache ich das nun darüber, starte die b-Schleife und drücke dann zwischendurch den A-Knopf, bekomme ich eine Memory Corruption.

Code: Alles auswählen

*** glibc detected *** /home/xxyyzz/workspace/Qt_Thread/Qt_Thread: malloc(): memory corruption (fast): 0xb6b00947 ***
Stinkt eigentlich nach nicht vernünftiger Kapselung. Sehe da den Fehler aber nicht. Hat jemand einen Tipp, bitte?
franzf
Beiträge: 3114
Registriert: 31. Mai 2006 11:15

Beitrag von franzf »

Du manipulierst aus einem Thread heraus ein Gui-Element - das darfst du nicht. Stattdessen solltest du das über SIGNAL/SLOT+QueuedConnection machen. Im Thread ein emit newValue() und in der Gui dann dieses Signal auf einen passenden Slot legen, in dem dann wieder die Gui verändert werden kann.

Gui verändern heißt zeichnen - und zeichnen darfst du in Qt nur im Hauptthread. Ein label.setText() triggert gleich einen repaint, der dann im Context des aufrufenden Threads ausgeführt wird. Wenn das ein anderer als der Hauptthread ist, dann wird in dem gezeichnet.

Pass auch noch auf, dass dein Thread-Objekt den Scope überlebt -> dynamische Speicherverwaltung.

// edit: Deine globale Variable ist richtig böse. Existieren mehrere Objekte von Qt_Thread, kann so einiges passieren, vor allem arbeitet dein Thread mit einem komplett anderen Objekt zusammen, als das das den Thread startet - und das willst du ja.
Außerdem sind globale Variablen immer böse und sollten nur im allerhintervorletzten Notfall eingesetzt werden, wenn garantiert nix anderes geht - das ist hier nicht der Fall. Mit SIGNAL/SLOT brauchst du eh kein "q" mehr.
pbooster2000
Beiträge: 10
Registriert: 25. November 2010 17:55

Beitrag von pbooster2000 »

franzf hat geschrieben:Du manipulierst aus einem Thread heraus ein Gui-Element - das darfst du nicht. Stattdessen solltest du das über SIGNAL/SLOT+QueuedConnection machen. Im Thread ein emit newValue() und in der Gui dann dieses Signal auf einen passenden Slot legen, in dem dann wieder die Gui verändert werden kann.
Danke dafür, das habe ich jetzt verstanden. Es treten keine Fehler mehr während der Laufzeit auf, da ich jetzt Signal/Slots benutze.
franzf hat geschrieben: Pass auch noch auf, dass dein Thread-Objekt den Scope überlebt -> dynamische Speicherverwaltung.
Guter Tipp. Hatte das vorhin so hingeschrieben, um mein Problem zu verdeutlichen. In meinem "richtigen" Programm werde ich den Scope beachten.
franzf hat geschrieben: // edit: Deine globale Variable ist richtig böse. Existieren mehrere Objekte von Qt_Thread, kann so einiges passieren, vor allem arbeitet dein Thread mit einem komplett anderen Objekt zusammen, als das das den Thread startet - und das willst du ja.
Außerdem sind globale Variablen immer böse und sollten nur im allerhintervorletzten Notfall eingesetzt werden, wenn garantiert nix anderes geht - das ist hier nicht der Fall. Mit SIGNAL/SLOT brauchst du eh kein "q" mehr.
q hatte ich noch übersehen, das ist jetzt weg. Globale Variablen in Klassen stinken, stimmt. Ich werde versuchen, diese rauszunehmen.

Danke Dir für die tolle Hilfe. Der Rechenprozess läuft jetzt recht schnell und die GUI ist dabei auch gut ansprechbar.

Mein Beispiel sieht jetzt so aus und scheint gut zu funktionieren (für nur eine Qt_Thread-Instanz und eine MyCalc-Instanz):

Code: Alles auswählen

#include <QThread>
#include <QMessageBox>
#include "qt_thread.h"

int a = 0;
int b = 0;

QThread *t;
MyCalc *c;





MyCalc::MyCalc(QObject* parent) :
        QObject(parent)
{

}

void MyCalc::myLoop()
{
	b = 0;
	float tmp = 0.0;


	while (b < 300000000){
		tmp = 6.5192412518215212 * (tmp+1) / 2.6315211121893 + b * b;

		b++;

		if (! (b % 100000000) ){
			emit incrementedB();
		}
		tmp = 0.0;

	}

	emit finishedLoop();
}



void Qt_Thread::updateMyB()
{
	ui.lbb->setText( QString("%1").arg(b) );
}



Qt_Thread::Qt_Thread(QWidget *parent)
    : QWidget(parent)
{
	ui.setupUi(this);

	t = new QThread();
	t->start();

	c = new MyCalc();
	c->moveToThread(t);

	connect(ui.pbWithThread, SIGNAL(clicked()), c, SLOT(myLoop()));
	connect(c, SIGNAL(finishedLoop()),this, SLOT(afterLoop()));
	connect(c, SIGNAL(incrementedB()), this, SLOT(updateMyB()) );


	connect(ui.pbIncA, SIGNAL(clicked()), this, SLOT(pbIncAClicked()));
	connect(ui.pbWithoutThread, SIGNAL(clicked()), this, SLOT(pbWithoutThreadClicked()));
}



void Qt_Thread::pbWithoutThreadClicked()
{
	b = 0;
	float tmp = 0.0;

	while (b < 300000000){
		tmp = 6.5192412518215212 * (tmp+1) / 2.6315211121893 + b * b;
		b++;
		if (! (b % 100000000) ){
			ui.lbb->setText( QString("%1").arg(b) );
			qApp->processEvents();
		}
		tmp = 0.0;

	}

	ui.lbb->setText( "Fertig :)" );
}

void Qt_Thread::pbWithThreadClicked()
{

}

void Qt_Thread::setb(const int b)
{
	ui.lbb->setText( QString("%1").arg(b) );
}

void Qt_Thread::pbIncAClicked()
{
	a++;
	ui.lba->setText( QString("%1").arg(a) );
}

void Qt_Thread::afterLoop()
{
	ui.lbb->setText("Fertig! :)");
}


Qt_Thread::~Qt_Thread()
{

}
pbooster2000
Beiträge: 10
Registriert: 25. November 2010 17:55

Beitrag von pbooster2000 »

Messageboxes innerhalb des Threads zu erstellen oder auf GUI-Elemente lesend zuzugreifen sollte aber erlaubt sein, oder?
DBGTMaster
Beiträge: 190
Registriert: 19. August 2010 10:00

Beitrag von DBGTMaster »

pbooster2000 hat geschrieben:Messageboxes innerhalb des Threads zu erstellen oder auf GUI-Elemente lesend zuzugreifen sollte aber erlaubt sein, oder?
wozu eine messagebox? Um fehlermeldungen auszugeben? Da würd ich ebenso ein signal error(qstring message) zurückgreifen, und die gui selber produziert dann die messagebox..

Lesen sollte meiner Meinung mach kein problem darstellen..
solarix
Beiträge: 1133
Registriert: 7. Juni 2007 19:25

Beitrag von solarix »

pbooster2000 hat geschrieben:Messageboxes innerhalb des Threads zu erstellen oder auf GUI-Elemente lesend zuzugreifen sollte aber erlaubt sein, oder?
Beides ist verboten.. die GUI-Elemente sind nicht threadsafe (was also, wenn die Daten gleichzeitig gelesen und geschrieben werden? Ausserdem ist es schlechtes OOP: was die GUI intern besitzt geht den Thread nichts an.

Aber: das ist ohnehin nicht notwendig. Einfach dem Thread alles was er braucht übergeben. Fehlermeldungen wie von DBGTMaster beschrieben mit Signals behandeln.

hth..
pbooster2000
Beiträge: 10
Registriert: 25. November 2010 17:55

Beitrag von pbooster2000 »

solarix hat geschrieben: Aber: das ist ohnehin nicht notwendig. Einfach dem Thread alles was er braucht übergeben. Fehlermeldungen wie von DBGTMaster beschrieben mit Signals behandeln.
Und wie macht man das, wenn während des Thread-Verlaufs auf den Status einer Checkbox zugegriffen werden muss? Dann kann ich den Zustand ja nicht bereits beim Threadstart als Parameter übergeben? Der könnte sich ja in der Zwischenzeit geändert haben (jemand hat auf die Checkbox geklickt)...

Danke für die Hilfe!
Christian81
Beiträge: 7319
Registriert: 26. August 2004 14:11
Wohnort: Bremen
Kontaktdaten:

Beitrag von Christian81 »

Wenn sich ein GUI-Element ändert kann der GUI-Thread (==Hauptthread) dies dem Workerthread ja mitteilen. Der Hauptthread muss ja sowieso entscheiden ob ggf. der Thread aufgrund der neuen Situation neu gestartet werden muss etc.
MfG Christian

'Funktioniert nicht' ist keine Fehlerbeschreibung
franzf
Beiträge: 3114
Registriert: 31. Mai 2006 11:15

Beitrag von franzf »

Der click auf die Checkbox löst ein Signal aus, auf das der Thread dann reagieren kann. Der Thread speichert den neuen Status der Checkbox ab und fertig. Und wie immer aufpassen, dass bei der Connection eine QueuedConnection zustande kommt.

Ich würde es wirklich nicht riskieren, über mehrere Threads hinweg ungeschützte Objekte anzufassen. Das ist einfach nur undefiniertes Verhalten und kann im Suizid deines Programms enden :P
pbooster2000
Beiträge: 10
Registriert: 25. November 2010 17:55

Beitrag von pbooster2000 »

Wenn ich tatsächlich nur einen einzigen Thread starte (zusätzlich zum GUI-Thread), sollte das kein Problem sein, oder? Auch wenn das nicht "schön" aussieht...
solarix
Beiträge: 1133
Registriert: 7. Juni 2007 19:25

Beitrag von solarix »

doch... das ist ein Problem.. und zwar nicht nur ein theoretisches, sondern ganz real. Im Grunde kannst du tun und lassen was du möchtest (wir können dir ja nichts verbieten), aber wehe du startest in ein paar Tagen ein Thread mit dem Topic "Hilfe: ich habe sporadische, nicht reproduzierbare Segfaults"...
Antworten