Mutexes und Destruktor-Reihenfolge



  • Mahlzeit,

    Ich habe ein Programm mit mehreren Threads, und möchte nun gerne verhindern, daß ein Thread ein Objekt löschen kann, während ein anderer gerade eine Member-Funktion desselben ausführt (es gibt nur die eine Funktion, die aus dem anderen Thread heraus aufgerufen wird).

    Mein Gedanke ging in die Richtung von sowas hier:

    struct Base
    {
        virtual void foo() = 0;
    
        void bar() {
            _m.lock();
            foo();
            _m.unlock();
        }
    
        ~Base() {
            _m.lock();
        }
    
      private:
        mutex _m;
    };
    
    struct Derived
      : public Base
    {
        void foo() { /*...*/ }
    };
    

    Das Problem hierbei ist natürlich, daß der Destruktor von Derived vor dem von Base ausgeführt wird. D.h. beim Locking des Mutexes ist es schon zu spät, denn das eigentliche Objekt ist ja schon weg.

    Ich könnte jetzt sicherlich hingehen, und das Locking in den Destruktor von Derived verschieben. Lieber wäre es mir aber, wenn ich Derived nicht verändern müßte, d.h. die Klasse weiß praktisch nichts von dem Mutex. Gibt es eine Möglichkeit, wie ich das einigermaßen geschickt lösen könnte?


  • Mod

    Die Lebenszeit eines Mutex darf niemals kürzer sein als die Lebenszeit des Objektes, das er sichert (es sei denn, er ist bei der Zerstörung des Objektes Aliasfrei, weil z.B. alle anderen Threads, die potentiell auf das Objekt zugreifen würden, schon beendet wurden - dann bedarf es der Sicherung nicht).
    Obwohl der Gedanke verständlich ist: ein lock im Destruktor ist definitiv ein Fehler.
    Stell dir vor, was passiert, wenn Thread1 bar aufruft, aber noch vor Aufruf des lock unterbrochen wird. Nun komme Thread2 daher und zerstöre das Objekt. Thread1 macht weiter und betreibt Leichenfledderei.



  • camper schrieb:

    Die Lebenszeit eines Mutex darf niemals kürzer sein als die Lebenszeit des Objektes, das er sichert

    Ist sie das denn hier? Ich meine, das Mutex bleibt als Member der Klasse doch so lange bestehen wie das Objekt, zu dem es gehört. Oder habe ich falsch verstanden was du meintest?

    Obwohl der Gedanke verständlich ist: ein lock im Destruktor ist definitiv ein Fehler.
    Stell dir vor, was passiert, wenn Thread1 bar aufruft, aber noch vor Aufruf des lock unterbrochen wird. Nun komme Thread2 daher und zerstöre das Objekt. Thread1 macht weiter und betreibt Leichenfledderei.

    Autsch... Du hast natürlich Recht, den Fall hatte ich überhaupt nicht bedacht.

    Aber wie macht man es denn dann richtig? Sollte ich das Locking besser außerhalb des Objekts machen, also überall da, wo bar() aufgerufen wird, bzw. Objekte gelöscht werden?


  • Mod

    dooooomi schrieb:

    camper schrieb:

    Die Lebenszeit eines Mutex darf niemals kürzer sein als die Lebenszeit des Objektes, das er sichert

    Ist sie das denn hier? Ich meine, das Mutex bleibt als Member der Klasse doch so lange bestehen wie das Objekt, zu dem es gehört. Oder habe ich falsch verstanden was du meintest?

    Schlecht formuliert von mir. Jedenfalls muss das Mutexobjekt solange existieren, wie irgendein Thread potentiell darauf zugreifen könnte. Die threadsichere Zerstörung kannst du relativ einfach per shared_ptr/weak_ptr erreichen - da das Objekt erst zerstört wird, wenn die letzte (starke) Referenz darauf erlischt, hast du kein Alias-Problem und ein zusätzliches Locking erübrigt sich. Schwierig wird es nur, wenn ein ganz bestimmter Thread die Zerstörung durchführen soll.



  • camper schrieb:

    Die threadsichere Zerstörung kannst du relativ einfach per shared_ptr/weak_ptr erreichen - da das Objekt erst zerstört wird, wenn die letzte (starke) Referenz darauf erlischt, hast du kein Alias-Problem und ein zusätzliches Locking erübrigt sich. Schwierig wird es nur, wenn ein ganz bestimmter Thread die Zerstörung durchführen soll.

    Ok, dann habe ich wohl genau den schwierigen Fall erwischt :).

    Der Thread, der bar() aufruft, darf keine Objekte löschen (Echtzeit-Anforderungen. Ich weiß, das lock() darf ich so wie oben dann auch nicht machen, aber das war ja nur als vereinfachtes Beispiel gedacht).
    Außerdem darf immer nur ein Derived-Objekt existieren, so daß ich nicht darauf warten kann, daß "irgendwann" die letzte Referenz wegfällt. Ich muß in jedem Fall sicherstellen, daß das Objekt gerade nicht benutzt wird (also ggf. warten, bis bar() beendet ist), und dann nacheinander erst das alte Objekt löschen, und dann ein neues erzeugen.

    Ich sehe schon, daß ich da einiges an meinem Code gründlich werde umkrempeln müssen...



  • Hi,

    ich habe den Eindruck, dass Du da vor einiger Zeit "falsch abgebogen" bist.

    dooooomi schrieb:

    ...Der Thread, der bar() aufruft, darf keine Objekte löschen (Echtzeit-Anforderungen. ...

    "Löschen verhindern" hat eigentlich nichts mit "Threads" zu tun. Das kann man relativ einfach erreichen, indem man Erzeugung und Vernichtung in eine Factory auslagert (Kon- und Destruktoren => protected, Factory friend).
    DIE kann dann entscheiden, ob sie eine Löschanfrage zulässt oder abblockt.

    dooooomi schrieb:

    ...Außerdem darf immer nur ein Derived-Objekt existieren, ...

    Wo ist hier die Betonung ?
    "darf immer nur ein Derived-Objekt existieren, "
    "darf immer nur ein Derived -Objekt existieren, "
    Ersteres kannst Du ebenfalls über die Factory abdecken, Zweiteres über eine abstrakte Basisklasse.

    Gruß,

    Simon2.



  • Simon2 schrieb:

    dooooomi schrieb:

    ...Der Thread, der bar() aufruft, darf keine Objekte löschen (Echtzeit-Anforderungen. ...

    "Löschen verhindern" hat eigentlich nichts mit "Threads" zu tun. Das kann man relativ einfach erreichen, indem man Erzeugung und Vernichtung in eine Factory auslagert (Kon- und Destruktoren => protected, Factory friend).
    DIE kann dann entscheiden, ob sie eine Löschanfrage zulässt oder abblockt.

    Eine Factory würde mir aber doch auch nur helfen, den Code zum Erzeugen und Vernichten der Objekte (bzw. zur Absicherung des Ganzen) an einer Stelle zu sammeln, anstatt daß das quer über's Programm verteilt ist, oder?



  • dooooomi schrieb:

    ...
    Eine Factory würde mir aber doch auch nur helfen, den Code zum Erzeugen und Vernichten der Objekte (bzw. zur Absicherung des Ganzen) an einer Stelle zu sammeln, anstatt daß das quer über's Programm verteilt ist, oder?

    Aber das ist doch auch das, was Du willst, oder ?
    Die Factory kann dann ja auch die notwendige Überprüfung übernehmen (irgendwie willst Du ja zwischen "dürfen"- und "dürfen nicht"-Threads entscheiden ...).

    Gruß,

    Simon2.



  • Simon2 schrieb:

    dooooomi schrieb:

    ...
    Eine Factory würde mir aber doch auch nur helfen, den Code zum Erzeugen und Vernichten der Objekte (bzw. zur Absicherung des Ganzen) an einer Stelle zu sammeln, anstatt daß das quer über's Programm verteilt ist, oder?

    Aber das ist doch auch das, was Du willst, oder ?

    Je mehr ich darüber nachdenke, eigentlich schon.

    Aber kann das Mutex überhaupt Member des zu schützenden Objekts sein? Auch wenn ich das Locking vor dem Löschen mache (also nicht erst "währenddessen" im Destruktor), dann bleibt das Problem, daß bar() vor dem lock() unterbrochen werden könnte, doch bestehen, oder nicht?


  • Mod

    dooooomi schrieb:

    Simon2 schrieb:

    dooooomi schrieb:

    ...
    Eine Factory würde mir aber doch auch nur helfen, den Code zum Erzeugen und Vernichten der Objekte (bzw. zur Absicherung des Ganzen) an einer Stelle zu sammeln, anstatt daß das quer über's Programm verteilt ist, oder?

    Aber das ist doch auch das, was Du willst, oder ?

    Je mehr ich darüber nachdenke, eigentlich schon.

    Aber kann das Mutex überhaupt Member des zu schützenden Objekts sein? Auch wenn ich das Locking vor dem Löschen mache (also nicht erst "währenddessen" im Destruktor), dann bleibt das Problem, daß bar() vor dem lock() unterbrochen werden könnte, doch bestehen, oder nicht?

    weshalb du ein Ownership-Modell unabhängig vom Mutex benötigst. Ein shared_ptr leistet das. Den deleter kann man dann so gestalten, dass er, anstatt das Objekt selbst zu löschen, der Factory Bescheid sagt (die für diesen Zweck intern eine Referenz auf das Objekt unabhängig vom shard_ptr hält). Also statt "der Letzte macht das Licht aus": "der Letzte sagt beim Pförtner Bescheid"...


Log in to reply