Threadgestaltung bei erwünschtem Abbruch



  • Hi,

    ich habe zwar ein ähnliches Topic im QT-Forum eröffnet und nutze auch QThread, allerdings glaube ich, dass die eigentliche Frage gar nicht im QT-Kontext steht. Daher wende ich mich vertrauensvoll an dieses Forum. Ich hoffe, das ist ok, hi!

    Also, ich habe folgendes Problem:

    Es gibt eine externe Funktion, die ziemlich lange für Berechnungen brauchen kann. Ich nenne Sie Mal void calculate(const std::string& input1, const std::string& input2) .

    Weiters hab ich ein UI, in dem Eingabeparameter für calculate geändert werden können. Jedes Mal, wenn sie geändert werden, soll eine neue Berechnung für calculate initiiert werden, während die alten Berechnungen aber gestoppt werden sollen.

    Jetzt habe ich ein wenig herumexperimentiert und Folgendes probiert, um mein Problem zu lösen:

    Falls ein Thread läuft und eine Berechnung gemacht werden soll (d.h. vorangegangene Änderung der Eingabeparameter), soll der alte Thread terminiert werden. Wenn er terminiert ist (gibt da son Signal für), soll ein neuer gestartet werden mit den neuen Parametern. Der alte wird dabei überschrieben.

    Leider klappt das alles nicht so, wie ich will, und laut QT gibt es einen Deadlock, den ich noch nicht gefunden habe.

    Bevor ich jetzt weitermache: Was würdet ihr vorschlagen wie ich das Problem hier besser lösen kann? Ich habe überlegt, einfach einen vector von Threads zu machen. Dann werden die alten terminiert, gleichzeitig wird aber der neue gestartet. D.h. im Gegensatz zu vorher gibt es nicht nur ein Thread-Objekt gleichzeitig.

    Denkt ihr, das ist eine gute Idee? Falls mehr Infos notwendig sind, tut mir Leid, ich liefere die auf Anfrage sofort nach.

    Vielen Dank im Voraus!



  • Hi!,

    schleierhaft ist mir, wieso du Threads verwendest, wenn es doch gerade im wunderschönen Qt Signals und Slots gibt. QTextEdit (und natürlich alle seine Söhne und Töchter) haben ein textChanged() Signal, darauf kannst du ja bauen.

    Oder ich missverstehe dich.

    Edit: Ah, stop. Jetzt sehe ich, du meinst die Performance wird runtergezogen wenn der User mehrere Male hintereinander was ändert? Nun, in diesem Fall... lass mich nachdenken.

    Und passiert dasselbe auch mit std::, bzw. boost::thread?



  • Hi,

    signals und slots sind ja synchron. Ich will aber asynchron arbeiten, denn calculate kann je nach Input mehrere Minuten dauern. Aber mit dem UI lassen sich viele schöne Dinge gestalten... nicht aber, wenn ich calculate im Hauptthread aufrufe.

    Na ja und das möchte ich jetzt eben umgehen. QThread oder boost::thread ist weniger das Problem; vielmehr interessiert mich wie ich meine Threads gestalte, sodass ich in einem "sicheren" Umfeld den Thread terminieren kann und einen neuen eben starte. 🙂



  • Ok, also Mal kurzes Update über meine lustigen Fehler. Korrigiert mich, wenn meine Erkenntnisse falsch sind:

    - QThread liefert ein signal, wenn der Thread terminiert wird (da man sagen kann, dass der Thread terminieren soll, er es aber trotzdem nur mit (sehr kurzer) Verzögerung tut). Dieses Signal und damit jeder Aufruf des Slots läuft aber noch im terminierten Thread. Daher kann ich nicht im Slot den Thread aus meinem Vector von Threads löschen, weil QThread nicht gelöscht werden darf, wenn der Thread noch läuft.

    - Also war meine Idee einfach bei einer Inputänderung alle bisher laufenden Threads zu terminieren (bzw. zu sagen, dass sie sich terminieren sollen) und alle, die nicht mehr laufen, rauszuschmeißen, danach wird eben der neue Thread hinzugefügt

    - Nächstes Problem war, dass ich die Berechnungsmethode natürlich nicht zwei Mal parallel aufrufen kann, weil die lokalen Variablen ja geshared werden... ich dachte irgendwie, jeder Aufruf erzeugt seinen eigenen Stack, sodass lokale Variablen nicht als gemeinsam genutzte Ressource gelten, aber da lag ich wohl falsch? (hier bin ich unsicher, bitte bestätigen!)

    - Also Mutex drum... aber wenn der Thread terminiert, wird der Mutex nicht geschlossen und dann hab ich im nächsten Berechnungsthread einen Deadlock...

    => Ich komme nicht drum herum meiner Berechnungsmethode (extern) eben via Referenz ein Flag mitzugeben, dass diese Berechnungsmethode hin und wieder überprüft und dann abbricht. Also kein terminate mehr 😞 Leider sorgt das für Rechenverzögerungen, aber das muss man dann wohl in Kauf nehmen. Oder ich mach die Abfrage permanent, aber das saugt zu viel Performance aus dem Berechnungsalgorithmus.

    Danke nochmals!



  • Eisflamme schrieb:

    - Nächstes Problem war, dass ich die Berechnungsmethode natürlich nicht zwei Mal parallel aufrufen kann, weil die lokalen Variablen ja geshared werden... ich dachte irgendwie, jeder Aufruf erzeugt seinen eigenen Stack, sodass lokale Variablen nicht als gemeinsam genutzte Ressource gelten, aber da lag ich wohl falsch? (hier bin ich unsicher, bitte bestätigen!)

    Ja, das war falsch gedacht. Das nennt sich auch thread-safety und dafür benötigt man einen Mutex, bzw. Semaphor. Aber so genau weiß ich nicht, ob das das richtige für dich ist.

    Edit: Hab den nächsten Stichpunkt nicht gelesen ^^ Aber was meinst du mit deadlock? Wenn der Zähler 0 ist ist er 0. Und einen Mutex kann man schließen wann man will.



  • Eisflamme schrieb:

    - Nächstes Problem war, dass ich die Berechnungsmethode natürlich nicht zwei Mal parallel aufrufen kann, weil die lokalen Variablen ja geshared werden... ich dachte irgendwie, jeder Aufruf erzeugt seinen eigenen Stack, sodass lokale Variablen nicht als gemeinsam genutzte Ressource gelten, aber da lag ich wohl falsch? (hier bin ich unsicher, bitte bestätigen!)

    Doch, jeder Thread hat seinen eigenen Stack.

    - Also Mutex drum... aber wenn der Thread terminiert, wird der Mutex nicht geschlossen und dann hab ich im nächsten Berechnungsthread einen Deadlock...

    Ich verstehe dein Problem nicht ganz. Schießt du den Thread mit brachialer Gewalt mitten in einer Berechnung ab, sodass er nicht mehr aufräumen kann? Setze im Worker Thread an den richtigen Stellen Interruption Points, sodass er sich selbst auf Anfrage in einem konsistenten Zustand beenden kann.



  • Ich verstehe dein Problem nicht ganz. Schießt du den Thread mit brachialer Gewalt mitten in einer Berechnung ab, sodass er nicht mehr aufräumen kann?

    Genau und diese Interruption Points wollte ich vermeiden, weil ich dafür noch etwas anderes anfassen muss. Aber eigentlich macht das nichts und ist der bessere Weg, deswegen setze ich das dann auch so um. 🙂

    Aber jetzt muss ich gerade Mal nachfragen, auch wenn's dämlich klingt: Wenn eine Funktion aufgerufen wird, dann liegen die Parameter nur einmal im Speicher? Werden die denn dann bei Funktionsaufruf erzeugt und nach dem letzten Aufruf wieder zerstört? Und sind für die lokalen Variablen feste Speicher zugeordnet oder werden die auch dynamisch bei Aufruf erzeugt?

    Mein Denkfehler ging etwas in die Richtung, dass ich das mit Rekursion verglichen habe, glaube ich. Da wird eine Funktion ja auch mehrfach aufgerufen, doch dort werden ja neue Variablen angelegt. Bei Multithreading aber nicht, also ist das Anlegen von lokalen Variablen/Parametervariablen ja nicht auf Funktionsaufrufe bezogen.

    Kann mir das jemand kurz erklären oder geht das so weit? Falls letzteres, hat jemand einen guten Artikel zu dem Detailpunkt?



  • Eisflamme schrieb:

    Mein Denkfehler ging etwas in die Richtung, dass ich das mit Rekursion verglichen habe, glaube ich. Da wird eine Funktion ja auch mehrfach aufgerufen, doch dort werden ja neue Variablen angelegt. Bei Multithreading aber nicht, also ist das Anlegen von lokalen Variablen/Parametervariablen ja nicht auf Funktionsaufrufe bezogen.

    Doch, die verhalten sich da gleich. Funktionsparameter werden direkt vor dem Funktionsaufruf auf den Stack gepusht und dementsprechend nach Verlassen des Funktionskörpers wieder vom Stack gepoppt. Also können sich verschiedene Threads ebenso wenig in die Quere kommen wie das bei einem rekursiven Aufruf der Fall ist.



  • Und das bezieht sich auch auf die lokalen Variablen, die innerhalb einer Funktion vorkommen?

    Denn irgendwie ist ein Zähler über das Ende der for-Schleife hinausgelaufen, als zwei Threads gleichzeitig die Funktion genutzt haben. Das habe ich Mal darauf geschoben.

    Das würde bei einer statischen Variable auch Sinn machen, doch statisch war sie nicht.



  • WAS wird denn eigentlich berechnet, und wohin kommen die Ergebnisse? Denn die sicherste Variante wäre: Pack die Berechnungen in einen eigenen Prozess. Prozesse kannst du problemlos abschießen, das Betriebssystem kümmert sich dann um den Rest (Aufräumen, etc.)

    Im Qt-Thread hast du übrigens geschrieben, dass die Funktion in einer Bibliothek steckt (ich nahm damit sofort an, sie liegt außerhalb deiner Zugriffsmöglichkeit), darum oben der verspätete Hinweis auf Prozess. Jetzt schreibst du aber, dass du sie um solche Checks erweitern willst (und wohl doch kannst). Nun gut, sicher die bessere Lösung.



  • Eisflamme schrieb:

    Und das bezieht sich auch auf die lokalen Variablen, die innerhalb einer Funktion vorkommen?

    Ja.

    Denn irgendwie ist ein Zähler über das Ende der for-Schleife hinausgelaufen, als zwei Threads gleichzeitig die Funktion genutzt haben. Das habe ich Mal darauf geschoben.

    Dann werden sie wohl eine gemeinsam genutzte Ressource haben, wenn der Fehler nicht single-threaded auftreten kann. Code?



  • Was helfen wuerde, sind kill-safe Datenstrukturen. Gibt es aber nicht in C++ und muss selbst gebastelt werden.

    Denn irgendwie ist ein Zähler über das Ende der for-Schleife hinausgelaufen, als zwei Threads gleichzeitig die Funktion genutzt haben. Das habe ich Mal darauf geschoben.

    Klingt wie wildes Rumraten ... das hilft nicht.



  • "Abschießen" ist nie eine gute Lösung, egal ob Thread oder Prozess.

    Wieso musst du unbedingt den Thread beenden und einen neuen erzeugen? Ich würd einfach einen Worker Thread machen und den auf einer Condition Variable oder Semaphore oder so schlafen lassen bis es was zu tun gibt...



  • dot schrieb:

    Ich würd einfach einen Worker Thread machen und den auf einer Condition Variable oder Semaphore oder so schlafen lassen bis es was zu tun gibt...

    Dann kannst du eine laufende Berechnung aber nicht mehr stoppen, wenn sich die Eingabe verändert hat. Interessant wäre natürlich, ob man die bisher berechneten Daten wiederverwenden könnte, um die Berechnungszeit zu reduzieren.



  • Michael E. schrieb:

    dot schrieb:

    Ich würd einfach einen Worker Thread machen und den auf einer Condition Variable oder Semaphore oder so schlafen lassen bis es was zu tun gibt...

    Dann kannst du eine laufende Berechnung aber nicht mehr stoppen, wenn sich die Eingabe verändert hat. Interessant wäre natürlich, ob man die bisher berechneten Daten wiederverwenden könnte, um die Berechnungszeit zu reduzieren.

    Das ist imo ein unabhängiges Problem. Ich würde das wohl über Work Packages lösen. Jedes Work Package bekommt ein abort Flag und gut ist. Der Thread hat eine Hauptschleife in der er schläft bis ein WorkPackage da ist und dann das WorkPackage abarbeitet. Mit einer Queue und einer CV/Semaphore sollte das eigentlich ziemlich einfach sein...



  • dot schrieb:

    Ich würde das wohl über Work Packages lösen. Jedes Work Package bekommt ein abort Flag und gut ist. Der Thread hat eine Hauptschleife in der er schläft bis ein WorkPackage da ist und dann das WorkPackage abarbeitet. Mit einer Queue und einer CV/Semaphore sollte das eigentlich ziemlich einfach sein...

    Das ist sicherlich auch eine vernünftige Lösung, aber ich sehe den Vorteil noch nicht so recht. Zwar sparst du dir die Kosten einer Threaderzeugung und einer Threadzerstörung, aber bei einer Berechnungsdauer von einer Minute fällt das nicht ins Gewicht. Auf der anderen Seiten musst du dich an potentiell mehreren Stellen in Schleifen und verschachtelten Funktionsaufrufen um das abort-Flag kümmern und eventuell noch aufpassen, dass man alles wieder ordnungsgemäß aufräumt, bevor man mit dem neuen Job beginnt. IMHO kann man hier den Komfort von Interruption Points nutzen, mit denen man sich um nichts mehr kümmern muss.



  • Michael E. schrieb:

    Zwar sparst du dir die Kosten einer Threaderzeugung und einer Threadzerstörung, aber bei einer Berechnungsdauer von einer Minute fällt das nicht ins Gewicht.

    Klar. Aber wenn du immer einen neuen Thread erzeugst kannst du das Problem bekommen, dass ein Thread noch nicht fertig ist bevor der neue schon startet. Du musst also entweder auf den alten warten oder die Datenstrukturen entsprechend auslegen, was beides eigentlich unnötig ist. Neuen Thread warten lassen vs. alten Thread am Leben halten macht imo keinen wirklichen Unterschied was den Code angeht. Ich find letztere Lösung aber sauberer und man spart sich das Hantieren mit mehreren Threads.

    Michael E. schrieb:

    Auf der anderen Seiten musst du dich an potentiell mehreren Stellen in Schleifen und verschachtelten Funktionsaufrufen um das abort-Flag kümmern und eventuell noch aufpassen, dass man alles wieder ordnungsgemäß aufräumt, bevor man mit dem neuen Job beginnt.

    Das musst du doch sowieso?

    Michael E. schrieb:

    IMHO kann man hier den Komfort von Interruption Points nutzen, mit denen man sich um nichts mehr kümmern muss.

    Ich muss gestehen ich bin mir nicht ganz sicher was du mit Interruption Points genau meinst, aber nach kurzem Googlen kann ich den konzeptionellen Unterschied zwischen Interruption Point und abort Flag nicht erkennen.



  • knivil schrieb:

    Was helfen wuerde, sind kill-safe Datenstrukturen. Gibt es aber nicht in C++ und muss selbst gebastelt werden.

    Dummerweise verwendet das OS selbst Datenstrukturen die nicht Kill-Safe sind.
    Libraries wie GDI oder USER haben z.T. Datenstrukturen "in" den Prozessen, die man sich damit zerschiessen kann. Bzw. Locks die nie mehr frei werden wenn man einen Thread zum ungünstigsten aller Zeitpunkte abschiesst.

    So zumindest sagt es die Fama.



  • dot schrieb:

    "Abschießen" ist nie eine gute Lösung, egal ob Thread oder Prozess.

    Naja, ... gute Idee würd ich's nicht gerade nennen, aber ich würde das Abschiessen von Prozessen auch nicht gerade als schlechte Idee bezeichnen.
    Das OS sollte auf jeden Fall gut damit klarkommen. Und wenn man weiss dass der Prozess beim Abschiessen auch keine Datenfiles kaputt machen wird, bzw. wenn das egal sein sollte, dann ... sehe ich da kein echtes Problem.



  • dot schrieb:

    Ich muss gestehen ich bin mir nicht ganz sicher was du mit Interruption Points genau meinst

    Das, was hier unter "Interruption" steht. Läuft im Prinzip auf ne Exception raus. Deshalb hab ich etwas übertrieben, als ich sagte, dass man sich an allen möglichen Stellen ums abort-Flag kümmern muss, weil man dieses Verhalten genauso mit einer Exception simulieren kann, die man in der Top-Level-Funktion des Threads fängt 😉 Ein nicht völlig bekiffter Programmierer sollte dann auch einen konsistenten Zustand für den Worker Thread wiederherstellen können.

    Wie sähe deine Vorgehensweise beim Abbrechen mit einer Queue und Condition Variable eigentlich aus?


Anmelden zum Antworten