Objekte im Speicher umkopieren



  • Hi, ich mache mir seit längerem Gedanken wie man ein Objekt (auf dem Heap) das in einem größeren Speicherblock (z.B. mittels placement new) erzeugt wurde umkopieren kann so dass andere Threads während des Kopiervorgangs den Status des ursprünglichen Objektes nicht mehr verändern können. Mein erster Gedanke war eine Pointer-Wrapper Klasse zu schreiben die den rohen Pointer auf das Objekt kapselt. Blöderweise kapselt z.B. der "T* operator->() const" Operator ja nicht den Memberzugriff bzw. Funktionsaufruf sondern gibt den rohen Zeiger zurück und führt die Aktion auf diesem aus. D.h. eine Wrapper Klassse kann meinem Verständnis nach für dieses Problem nicht funktionieren. Natürlich könnte man alle Funktionen/Operatoren der Pointer-Wrapper Klasse mit Lockguards vollpflastern um zu verhindern, dass Threads auf ein solches Objekt das zum Kopieren geblockt wurde, zugreifen können. Aber man kann nicht sicherstellen, dass nicht zuvor ein anderer Thread eine Funktion des Objektes aufgerufen hat die womöglich sehr lange dauert und noch immer auf diesem Objekt arbeitet während das Kopieren gestartet wird.

    Ich hoffe ich konnte das Problem einigermaßen verständlich erklären :).
    Jemand eine Idee für eine Lösung?



  • Variante 1: Bau eine Wrapper-Klasse die sämtliche Funktionen des Objekts forwarded und pack in alle solchen forwarding-Funktionen den Lock-Guard rein. Weiters muss der Wrapper sämtliche Funktionen anpassen wo Adressen auf Member zurückgegeben werden. Das muss alles auf Indexe, Handles o.Ä. umgestellt werden.

    Variante 2: Ein Protokoll wo die Objekte vor dem Zugriff explizit "ausgecheckt" werden müssen. D.h. statt einer "get" Funktion die lokal lockt und den Zeiger zurückgibt machst du eine "checkout" Funktion die lock_shared bzw. lock auf die Mutex aufruft, und dann ein RAII Helper Objekt zurückgibt welches die Adresse des Objekts enthält und im Destruktor unlock_shared bzw. unlock aufruft.

    Variante 3: Safepoints wie sie von den meisten "moving" GC Implementierungen verwendet werden. Falls dir das Konzept nicht bekannt ist: ein Safepoint ist quasi ein Zustand in dem sich ein Thread befinden kann, in dem grantiert ist dass eine bestimmte Operation (bzw. eine Klasse von Operationen) "sicher" ist. Das Protokoll könnte z.B. so aussehen:

    • Alle Threads die mit solchen Objekten arbeiten wollen müssen sich erstmal bei irgend einem Manager Objekt registrieren bevor sie solche Objekte angreifen dürfen.
    • Alle solchen Threads müssen dann periodisch eine "Safepoint" Funktion aufrufen - und zwar zu einem Zeitpunkt wo sie nicht gerade die Adresse eines solchen Objekts irgendwo in einer lokalen Variable/Temporary/Register halten.
    • Idealerweise sollten es dann noch "EnterSafeState" und "LeaveSafeState" Funktionen geben die die Threads z.B. verwenden wenn sie in eine länger dauernde Phase wechseln während der sie immer "safe" sind. Also z.B. bevor sie ein "sleep" machen oder eine IO Funktion aufrufen. Oder generell länger laufende Funktionen aufrufen von denen bekannt ist dass sie die Objekte um die es geht nicht angreifen können.
    • Sämtliche Zeiger auf solche Objekte müssen volatile oder besser atomic sein, damit garantiert ist dass der Compiler nicht Code erzeugt der die Adresse in einem Register zwischenspeichert.

    Wenn du dann das Objekt verschieben willst, dann bittest du den Manager alle Threads in einen safe statezu bringen. Der Manager kann dann die Threads beim nächsten Aufruf der "Safepoint" Funktion blockieren. Bzw. für die Threads die sich schon in einem "safe state" befinden muss sich der Manager nur merken dass er sie beim nächsten Aufruf der "LeaveSafeState" Funktion blockieren muss.

    Dann wartet der Manager einfach bis alle Threads "safe" sind.

    Danach kannst du anfangen deine Objekte zu verschieben.

    Und wenn du fertig bist teilst du dem Manager mit dass er die Threads wieder weiter laufen lassen kann.

    Ist natürlich um Grössenordnungen mehr Aufwand als die anderen Varianten, und wenn man es manuell implementieren muss auch viel viel fehleranfälliger. Der Vorteil ist dass beim Zugriff auf die Objekte nicht dauernd gelockt werden muss. Zahlt sich also nur aus wenn sehr häufig Zugriffe auf solche Objekte passieren, die Objekte eher selten verschoben werden, und die bessere Performance wirklich wichtig ist.



  • Hi, das sind schonmal gute Tipps. Das mit den Safepoints hatte ich im Bezug zu Garbage Collectoren schon gelesen konnte mir aber unter dem Begriff Safepoint nichts vorstellen was nun klarer ist. Aber wie du schon gesagt hast würde das viel manuellen Aufwand bedeuten was auch wieder neues Fehlerpotenzial bietet. Evtl. dann eine Kombination aus Punkt 1 und 3. Nochmal zu Punkt 1. Wie würde denn eine solche Wrapper Klasse aussehen, damit man die Funktionen des Objekts forwarden kann. Das wäre natürlich ein cooles Feature, aber ich wüsste gerade nicht wie man einen Funktionsaufruf einem Wrapper bekannt machen kann?



  • Mit 1 meine ich einfach das:

    class Foo {
    public:
        void doFooThings();
        ...
    };
    
    class FooWrapper {
    public:
        void doFooThings() {
            MyLockGuard g{m_lock};
            m_foo->doFooThings();
        }
        ...
    };
    


  • Das wäre natürlich ein inakzeptable großer Aufwand. Aber ich habe noch etwas von Stroustrup höchst persönlich gefunden. Er hat sich scheinbar auch schon den Kopf über dieses Problem zerbrochen. Aber da dieser Ansatz nicht mit Exceptions funktioniert leider auch nicht optimal. Aber es wird hier noch auf einen Tiemann Wrapper verwiesen. Werde mich mal schlau machen was es mit diesem auf sich hat. Der Overhead scheint auch vergleichsweise akzeptabel zu sein.

    https://stroustrup.com/wrapper.pdf



  • @Enumerator sagte in Objekte im Speicher umkopieren:

    Das wäre natürlich ein inakzeptable großer Aufwand.

    Das war aus deiner Fragestellung nicht ersichtlich.

    https://stroustrup.com/wrapper.pdf

    Der dort unter "5 Prefix and Suffix" beschriebene Code ist im Prinzip das was ich als Variante 2 beschrieben habe. Er verwendet dort halt den operator -> statt einer "get" bzw. "check out" Funktion. Ist aben ansonten equivalent.



  • Der Plan ist zum Scheitern verurteilt. Man kann nicht garantieren, dass irgend ein Thread eine Kopie auf das ursprüngliche Objekt vorhält. Daraus folgt dann, dass man das Original nicht zerstören kann. Der Aufwand mit dem Wrapper sorgt nur dafür, dass Threads das neue Objekt nutzen können, während länger laufende Threads weiterhin die alte Version nutzen werden. Es bleibt nur die Möglichkeit alle Threads zu stoppen, dann das Objekt zu zerstören und ein neues anzulegen, um sicherzustellen, dass das neue Objekt genutzt wird. Der ganze Aufwand mit den Wrappern kann man sich letztlich sparen.

    Was man noch machen könnte, wäre das OS dafür zu benutzen in dem man eine Speicherseite austauscht. Dann muss man aber aufpassen, dass das Objekt keine internen Status hat, der Probleme machen könnte.

    @hustbaer Mir dünkt, dass bei Deinem Vorschlag das double checked locking pattern Problem auftreten dürfte.


Log in to reply