Threadgestaltung bei erwünschtem Abbruch



  • 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?



  • Michael E. schrieb:

    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 😉

    Dacht ich mir. Das ist aber eben genau das was ich mich abort Flag meinte. Ob man da jetzt eine Exception wirft (was sicherlich sinnvoll ist) oder sonstwas tut ist dann ein Implementierungsdetail, es ändert nix daran dass man eben an allen möglichen Stellen checken muss ob der Thread aborted werden soll. 😉
    Ich denk dieser check lässt sich rein prinzipiell einfach nicht vermeiden.

    Michael E. schrieb:

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

    Na genau so. Die Queue und Condition Variable haben ja mit dem Abbrechen nix zu tun, sondern nur damit wie die Arbeit gestartet wird. Anstatt immer einen neuen Thread zu machen gibts eben einen Thread der in einer Schleife Arbeit von der Queue abholt.



  • dot schrieb:

    Das ist aber eben genau das was ich mich abort Flag meinte.

    Achso. Dann hab ich nichts gesagt.

    Michael E. schrieb:

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

    Na genau so. Die Queue und Condition Variable haben ja mit dem Abbrechen nix zu tun, sondern nur damit wie die Arbeit gestartet wird. Anstatt immer einen neuen Thread zu machen gibts eben einen Thread der in einer Schleife Arbeit von der Queue abholt.

    Ich meinte, wie du dem Worker Thread mitteilst, dass er seine aktuelle Berechnung abbrechen und sich die neuen Parameter anschauen soll.



  • Naja, ich würd wohl sowas machen:

    class Abortable
    {
    private:
      volatile bool is_aborted:
    
    protected:
      void check_abort() const
      {
        if (is_aborted)
          throw abort_exception();
      }
    
    public:
      void abort()
      {
        is_aborted = true;
      }
    };
    
    class Calculation : public Abortable
    {
    public:
      void calc()
      {
        // do stuff
    
        check_abort();
    
        // do stuff
    
        ...
      }
    };
    

    D.h. die Berechnung ist eben in einem Objekt gekapselt. Wenn eine neue Berechnung gestarted werden soll, dann wird eben so ein Objekt erzeugt, ein Zeiger drauf in die Queue gepushed und auf einer evtl. momentan laufenden Berechnung abort() aufgerufen.

    Aber prinzipiell ist das ja nicht so wichtig. Worum es ja eigentlich hier geht war das Beenden und was das angeht sind wir uns ja einig dass man eben im Thread einen Check (Flag oder gleich die Library Lösung per boost interruption point) brauchen wird 😉



  • Michael E. schrieb:

    Ich meinte, wie du dem Worker Thread mitteilst, dass er seine aktuelle Berechnung abbrechen und sich die neuen Parameter anschauen soll.

    Naja, Beispiel:

    TaskHandle h = queue.QueueTask(work);
    h.Cancel();
    

    😃

    OK, "work" muss das nun noch irgendwie mitbekommen. Wie man das implementiert, hängt nun davon ab, was QueueTask() als Parameter nimmt - also was "work" für nen Typ haben darf.
    Wenn man super-generisch sein will, und QueueTask() nen std::function<void()> frisst, dann wird's ein wenig kompliziert. Dann muss man wohl oder übel mit Thread-Local-Storage arbeiten.

    Wenn man dagegen einfach verlangt, dass jeder Task ("work") eine Schnittstelle ITask implementiert, dann kann diese ja z.B. einfach so aussehen:

    class ITask
    {
    public:
        // ...
        virtual void Run() = 0;
        virtual void Cancel() = 0;
    };
    

    Der Task wiederum kann das dann machen wie er möchte, ein atomic cancel-flag verwenden, bool + Mutex, was auch immer (oder den Cancel() Aufruf gleich ganz ignorieren).



  • Ah, ich dachte, da gibts dann irgend nen tollen Trick mit einer Condition Variable, den ich nicht kenne.



  • Achso nein, die CV war nur gedacht zum Schlafen auf der Queue 😉



  • @dot: Warum volatile? Wolltest du nicht atomic<bool> sagen?



  • Für ein Cancel-Flag reicht volatile mMn. aus.

    Es muss ja nicht sofort vom anderen Thread "gesehen" werden, und das Lesen/Schreiben muss normalerweise auch keine Memory-Order implizieren.
    Zumindest so lange man die Information "canceled" nicht mit irgendwas anderes verknüpft.
    Es reicht also, wenn wir sicherstellen, dass der Lese-/Schreibvorgang nicht komplett wegoptimiert wird. Und genau dafür ist volatile gut, wenn ich mich recht erinnere.

    Wenn der gecancelte Task natürlich im "if (Canceled())" irgendwas macht, was sich darauf verlässt dass bestimmte Dinge jetzt so-und-so sein müssen, weil "sonst wäre ich ja nicht gecanceled worden", dann wäre das natürlich böse.
    Ein Programm dass solche Abhängigkeiten enthält, würde ich aber schon mal grundsätzlich als problematisch bezeichnen, d.h. die Restriction dass man sowas eben nicht machen darf, halte ich für nicht sehr schlimm.

    Und da ein volatile Read/Write auf vielen Plattformen um Grössenordnungen billiger ist als ein atomic Read/Write...

    (Man vergebe mir das schlimme Denglisch in diesem Beitrag, aber es schert mich jetzt nicht das nochmal umzuschreiben *g*)



  • Nö ich meinte schon volatile, warum müsste der denn deiner Meinung nach atomic sein?

    EDIT: Genau was hustbaer sagt 😉



  • Warum habt ihr dann damals bei meinem (zugegebenermaßen hässlichen) IRC-Server so heftig protestiert?



  • Vermutlich weil volatile dort eben nicht ausreichend war? Manche Leute verwechseln volatile gern mit "threadsafe"...


Anmelden zum Antworten