Threadgestaltung bei erwünschtem Abbruch



  • Genau, bei neuem Task soll die Berechnung abgebrochen werden, weil die Ergebnisse sowieso obsolet wären.

    Mir fällt es noch etwas schwer solchen Code zu lesen, z.B. weiß ich gerade nicht, wofür GetTask genutzt werden soll. Und mich würde natürlich interessieren, wie das Setzen von einem Task geschieht. Geschieht das ohne Mutex und alles? Einfach task setzen und n Wake schießen?

    Und dann zu scoped_lock: Also in Process locked er den Mutex und in IsAbort() wartet er in IsAbort dann ja, bis Freigabe erfolgt, also hängt er erstmal da fest?



  • Das setzen der Tasks musst du natürlich schon mit gelockter Mutex machen.
    Und in Process() wird der Lock ja temporär wieder freigegeben. Sonst würde das ja wirklich alles blockieren.

    Soll auch nicht mehr als eine schnelle Skizze sein wie man sowas implementieren kann.

    Und was GetTask() macht: na so lange warten bis es einen Task abzuarbeiten gibt ODER die Queue zerstört werden soll ("abort"), und den Task (bzw. bei abort eben 0) dann zurückgeben.



  • Ja okay, aber ich meine jetzt - und sorry, wenn ich mich da einfach doof anstelle -, dass doch schon beim Schleifenkopf in GetTaskNoLock eine Sperre entsteht? Ich meine, in Zeile 1 von GetTaskNoLock wird ein scoped_lock erstellt und im Schleifenkopf mit IsAbort() wird ein weiterer scoped_lock in IsAbort() angefasst. Hängt er dann nicht schon da? Oder macht das nichts, weil bei der Taskzuweisung mit dem Wake eben dort geweckt wird?

    Und die Frage auf GetTask meinte, wann das aufgerufen wird vom Benutzer o.ä. Der wird ja nur Compute() aufrufen oder halt den Task ändern.

    Vll. sollte ich mehr rumprobieren, um mich dem Thema anzunähern, mir kommt das unfassbar schwierig vor...



  • Err, hatte da noch nen Kleinen Schönheitsfehler drinnen.
    Task* Queue::GetTaskNoLock(Lock& lock) muss Task* Queue::GetTaskNoLock(scoped_lock& lock) heissen.

    Also...

    scoped_lock ist ne Hilfsklasse die im Konstruktor "Lock" auf die übergebene Mutex macht, und im Destruktor "Unlock".
    Dazwischen kann man selbst manuell "lock" bzw. "unlock" machen -- wobei scoped_lock allerdings immer nur "1x gelockt" sein kann. 2x "Lock" hintereinander ist also ein Fehler und wird mit nem ASSERT() "belohnt", 2x "Unlock" genau so.

    Was die ganzen "XxxNoLock" Funktionen angeht: die heissen so, weil sie selbst keinen Lock holen -- der Aufrufer muss sich darum kümmern. Das ist z.T. ne Performance-Optimierung, bzw. wenn man Condition-Variablen verwendet braucht man es auch öfters -- beim "wait" auf die Condition-Variable darf die Mutex ja nicht rekursiv gelockt sein.

    Queue::GetTaskNoLock() verlangt dabei vom Aufrufer dass eine Referenz auf das zum sperren der Mutex verwendete scoped_lock Objekt mitgegeben wird.
    Diese Referenz wird dann weiters an die wait() Funktion der CV übergeben. Diese wiederrum macht intern ein "unlock-wait-lock". D.h. während man wartet dass die CV signalisiert wird, ist die Mutex frei.



  • Danke für Erklärung und die ganze Geduld!

    Nur der Vollständigkeit halber, das Hinzufügen eines neuen Tasks ginge jetzt so?

    void setTask(Task& task)
    {
    {
    scoped_lock lock(m_mutex);
    m_nexttask = task;
    }
    m_queueNotEmptyCondition.notify_one();
    }
    

    ?

    Oh und - ich komme mit dem Verständnis so langsam weiter -, wie ist das denn am Ende der Schleife in Process: Der Mutex wird gelockt, dann kommt aber IsAbort im Schleifenkopf und er hängt dort. Aber soll der dort hängen? Wäre da nicht ein IsAbortNoLock() sinnvoller?



  • Eisflamme schrieb:

    Danke für Erklärung und die ganze Geduld!

    Nur der Vollständigkeit halber, das Hinzufügen eines neuen Tasks ginge jetzt so?

    void setTask(Task& task)
    {
    {
    scoped_lock lock(m_mutex);
    m_nexttask = task;
    }
    m_queueNotEmptyCondition.notify_one();
    }
    

    ?

    Ja, z.B.

    Also mal abgesehen von den ganzen Dingen die mit Threading nix zu tun haben, wie dass irgendwer die Tasks wieder freigeben muss, dass meine "Queue" gar keine Queue ist (weil sie immer nur einen "next task" beherrbergen kann) etc.
    Ich persönlich mache das signal() immer während die Mutex noch gelockt ist, aber das ist AFAIK nicht gefordert. Und ich würde hier notify_all machen, auch hauptsächlich aus Gewohnheit, und weil ja theoretisch ein zweiter Worker-Thread dazukommen könnte.
    EDIT: OK, das mit "Thread dazukommen" ist vermutlich Unsinn. Reicht ja wenn einer aufwacht. Trotzdem, so lange ich keinen besonders gute Grund sehe mit notify_one() zu arbeiten mache ich immer notify_all(). Spart mit Kopfschmerzen während ich (nicht) darüber nachdenken muss ob notify_one() auch sicher nicht dazu führen kann dass irgendwo irgendwie kein Thread mehr weitermacht, weil sich keiner zuständig gefühlt hat 🙂 /EDIT

    Oh und - ich komme mit dem Verständnis so langsam weiter -, wie ist das denn am Ende der Schleife in Process: Der Mutex wird gelockt, dann kommt aber IsAbort im Schleifenkopf und er hängt dort. Aber soll der dort hängen?

    Nö, soll er nicht. Muss er auch nicht, wenn die Mutex "rekursiv" ist. Also sich mehrfach vom selben Thread aus locken lässt. Was jetzt Vor- und Nachteile hat. (Vorteil: man muss weniger aufpassen. Nachteil: man übersieht schneller Fälle wo eine "doppelt" gelockte Mutex an das wait() einer CV übergeben wird, was dann meist böse endet)

    Wäre da nicht ein IsAbortNoLock() sinnvoller?

    Auf jeden Fall, hab' ich übersehen 🙂
    Ich korrigier das dann mal schnell...

    ps:

    Um Fehler zu vermeiden, bietet es sich an a) wenn's leicht geht eben nicht mit rekursiven Mutexen zu arbeiten und b) allen XxxNoLock Funktionen eine Referenz auf das scoped_lock Objekt als Parameter zu verpassen.
    Dadurch vermeidet man Unachtsamkeitsfehler, wo man XxxNoLock aufruft ohne die Mutex gelockt zu haben.



  • Okay, damit sind meine aktuellen Fragen beantwortet. Habe noch etwas herumgespielt mit QT- und Boost-Mutexen und habe das Gefühl es jetzt wirklich verstanden zu haben. Vielen Dank für die ganze Mühe 🙂


Anmelden zum Antworten