Flexibler Zugriff auf Ressourcen



  • Shade Of Mine schrieb:

    oder der Manager laesst sich nicht beenden solange er noch Objekte managed.

    Wie würdest du das umsetzen, wenn man kein Singleton erzwingen möchte? Wäre mein vorhin erwähnter Ansatz des Frontend-Managers halbwegs sinnvoll, oder gibts andere bewährte Möglichkeiten?

    Shade Of Mine schrieb:

    Kann man aber einfach umgehen indem man keine shared_ptr hergibt sondern eigene Smartpointer. Wie eben hier den ResourcePtr. Der ist mit Absicht kein shared_ptr (nehme ich mal schwer an).

    Ja, du erinnerst mich gleich an ein paar Gründe, wieso ich damals nicht shared_ptr genommen habe. Einer davon war aber auch, dass ich dessen Fähigkeiten etwas unterschätzt habe. Gut, dass das hier so kontrovers diskutiert wird 🙂

    Shade Of Mine schrieb:

    Dazu muss nur noch die Delete Policy geklaert werden. Mein Liebling ist: fuer jedes Acquire mach ein Release. Das loest alle Probleme hier schlagartig. [...] Acquire / Release. Immer dieses Paar verwenden und der Code wird viel viel einfacher.

    Dann würde sich eine Basisklasse mit diesen beiden virtuellen Funktionen wohl anbieten. Oder zwei einzelne Callbacks (z.B. std::tr1::function ). Wobei, ersteres ist einfacher und hat vermutlich weniger Overhead. Statische Polymorphie eignet sich nicht, weil dann die Smart-Pointer-Typen unterschiedlich werden... Oder hast du hier was ganz Anderes gemeint?

    Shade Of Mine schrieb:

    Und den Code enorm verkompliziert.

    Ne ne. ResourcePtr ist genau das richtige. Nexus braucht nur custom Deleter. Das ist alles.

    Danke für deine Einwände. Mir wäre es schon recht, wenn ich den ResourcePtr mehr oder weniger behalten könnte. Aber ein paar Nachteile hat das eben auch... Muss ich mir in Ruhe durch den Kopf gehen lassen und überlegen, wie konkrete Implementierungen aussehen könnten 🙂



  • Du würdest es also so lösen, dass Ressourcen ihren Manager am Leben halten, solange sie verwendet werden?

    Weiss nicht, ich kenne deine genauen Anforderungen nicht. Es gibt hier sehr viele mögliche Kräfte die in die eine oder andere Richtung ziehen können. Es ist auf jeden Fall eine Möglichkeit, und IMO keine grundsätzlich schlechte.

    Dann müsste man den ResourceManager aber selbst in einem shared_ptr speichern. Oder die ResourceManager-Klasse ist nur ein Frontend für den Benutzer, intern wird ein shared_ptr auf den tatsächlichen Verwalter gespeichert, der auch nach der Zerstörung des Frontends weiterleben kann.

    Richtig. Wobei die Frontend-Variante vermutlich besser ist. Das Frontend könnte z.B. den "Backend" mitteilen wenn es sterben geht. Das Backend könnte daraufhin von "retain" zu "release" wechseln. Oder assert()en wenn es noch referenzierte Resourcen gibt.

    Bisher war die Semantik so: Wenn das ResourceManager-Objekt stirbt, werden alle Ressourcen freigegeben und allfällige ResourcePtrs werden NULL – der ResourceManager besitzt also die Ressourcen. So werden Ressourcen auch garantiert freigegeben, es können keine Leaks (z.B. wegen zyklischer Referenzen) entstehen. Findest du das eher nicht sinnvoll?

    Ich noch kein solches Problem (Zyklen in Resource-Klassen). Würde ich nicht als sehr wichtig einstufen. Und mit nem assert() checken kann man es ja auch so (s.o.), sofern man vorschreibt dass der User den Manager erst löschen darf, wenn schon alle Resourcen freigegeben wurden.
    Wenn du es nicht als Feature brauchst, dass die Resourcen-Referenzen getrackt werden und deterministisch "genullt" werden können, würde ich sagen: vergiss das 🙂

    Ist es mit shared_ptr überhaupt möglich mitzuverfolgen, wer alles eine Referenz auf die Ressource hält? Man kann ja Kopien von shared_ptr-Objekten nicht überwachen, oder? Denn in diesem Fall könnte ich das automatische NULL-Setzen der shared_ptrs bei der Zerstörung des Managers ohnehin vergessen.

    Nein, richtig, richtig.

    hustbaer schrieb:

    * Entweder muss sich der Resource-Manager einen weak_ptr auf Objekte behalten, die er schonmal hergegeben hat, damit er bei weiteren Anfragen nach dem selben Objekt einen shared_ptr erzeugen kann der sich den selben "Counter" mit bereits existierenden shared_ptr teilt.

    Oder man verwendet hier gleich shared_ptr . Dann würde die Ressource bei der Strategie "behalte unbenutzte Ressourcen" automatisch allokiert bleiben, und bei "gib unbenutzte frei" könnte OnUnusedResource() den shared_ptr entfernen und damit die Freigabe einleiten. So wie ich das sehe, braucht man weder weak_ptr noch manuelles Reference Counting, oder?

    Da muss ich jetzt etwas ausholen.
    Ein shared_ptr ist besteht im Prinzip aus:
    * einem rohen zeiger auf *irgendwas* (*)
    * einer "shared reference" auf ein Objekt dass ich hier mal "Counter" nenne
    Der "Counter" besteht wiederum aus:
    * einem Zähler der die shared_ptr zählt die auf ihn zeigen
    * einem rohen zeiger
    * einem Deleter
    Einen shared_ptr erzeugen, der einen bereits bestehenden "Counter" verwendet, kann man nur, wenn man einen anderen shared_ptr oder weak_ptr hat, der diesen "Counter" verwendet.

    Und der Deleter wird erst ausgeführt, wenn der letzte shared_ptr verschwindet, der diesen "Counter" verwendet.

    Wenn du jetzt im Manager einen shared_ptr hältst, und alle shared_ptr den selben Counter verwenden, dann wird der Deleter niemals ausgeführt. Da der Manager ja immer noch einen shared_ptr mit diesem Counter hat. Und der Manager könnte auch nie sterben, da die Resource selbst einen shared_ptr auf den Manager hat -> Cycle -> Leak

    Wenn du im Manager dagegen weder einen shared_ptr noch einen weak_ptr hast, dann kannst du keine neuen shared_ptr erzeugen, die den selben Counter verwenden. Jeder "ausgegebene" shared_ptr hätte dann seinen eigenen Counter, und der Deleter würde mehrfach ausgeführt (1x pro Counter, und das erste mal u.U. viel zu früh). In der Variante müsstest du dann im Manager also mizzählen wie viele "Deleter Ausführungen" noch "offen" sind.

    Ohne mitzhälen geht also nicht mit shared_ptr, und auch nicht ohne shared_ptr/weak_ptr. Muss also ein weak_ptr her, anders geht es nicht.

    ----

    *: Es können verschiedene shared_ptr, die den selben "Counter" verwenden, ganz unterschiedliche "rohe Zeiger" speichern. Das ermöglicht u.A. shared_ptr zu casten, aber auch shared_ptr auf "innere" Objekte (z.B. Membervariablen) zu erstellen, die trotzdem das ganze Objekt am Leben halten. Im Prinzip kann man auch eine beliebige gültige Adresse übergeben.

    ----

    Ein paar weitere Gedanken:

    Zeiger auf Null zu setzen, wenn das Manager stirbt, ist nicht ohne grösseren Aufwand (Implementierung + Runtime-Overhead) möglich, wenn es threadsafe sein soll. Ausserdem kannst du Fälle nicht abdecken, in denen jmd. z.B. sowas macht (auch ohne multithreading):

    void DoStuff(Image* p)
    {
        p->Blah();
        DoSomeThing(); // <- und hier gibt das Programm den Manager frei (warum auch immer)
        p->Blubb();    // und hier knallts dann. oder auch nicht. UB halt.
    }
    
    void FunFunFun(MyPtr<Image> p)
    {
        DoStuff(p.get()); // Wir halten ja "p" während DoStuff() läuft, also ist alles gut
                          // (bloss eben leider doch nicht)
    }
    

    Statt zeiger Null zu setzen, könntest du auch die gemanagten Objekte in einen "leer" Zustand versetzen, ähnlich Dispose()/Close() in C#/Java. Wenn das threadsafe gehen soll ist das zwar auch einiges an Aufwand, aber weniger als beim "getrackte Zeiger Nullsetzen" Variante. Und der Oben gezeigte Problemfall lässt sich auch lösen.
    Nachteile: man braucht einen (mir immer sehr lästigen) Zombie-Zustand für alle Resource Klassen.
    Und wenn Multithreading ein Thema ist, müssen alle Klassen intern synchronisiert sein, damit keiner Dispose() sagen kann, während auf einem anderen Thread z.B. gerade eine andere Memberfunktion ausgeführt wird.

    ----

    Nochwas z.T. acquire/release (oder im COM jargon: AddRef/Release). Finde ich auch nicht übel. Wobei ich mir dann denke: wieso überhaupt mit Custom Deleter rummachen? In so einem Fall kann das Objekt selbst wissen, ob es sich beim "final release" löschen soll, oder doch nicht (weil es z.B. aufm Stack lebt).
    Dann braucht man weder eigene Smart-Pointer, noch muss man die Interfaces der Resourcen-Verwender-Klassen mit Smart-Pointern zumüllen. Der passende Smart-Pointer heisst dann boost::intrusive_ptr, und die Resourcen-Verwender-Klassen können sich jederzeit aus einem übergebenen rohen Zeiger einen neuen, funktionierenden boost::intrusive_ptr basteln, wenn sie das Objekt am Leben erhalten wollen.
    Die Verwendung von Resourcen kann dann einfach so aussehen:

    void foo()
    {
        Resource res;
        bar(&res);
    
        boost::intrusive_ptr<Resource> countedRes(new Resource(counted_tag)); // oder auch über ne abgeleitete TrackedResource Klasse - was auch immer
        bar(countedRes.get());
    }
    


  • hustbaer schrieb:

    Wenn du jetzt im Manager einen shared_ptr hältst, und alle shared_ptr den selben Counter verwenden, dann wird der Deleter niemals ausgeführt. Da der Manager ja immer noch einen shared_ptr mit diesem Counter hat.

    Stimmt, das war ein blöder Denkfehler meinerseits.

    hustbaer schrieb:

    Wobei ich mir dann denke: wieso überhaupt mit Custom Deleter rummachen? In so einem Fall kann das Objekt selbst wissen, ob es sich beim "final release" löschen soll, oder doch nicht (weil es z.B. aufm Stack lebt).

    Dann müsste ich aber die Anforderungen an die Ressourcenklassen erhöhen, die momentan recht niedrig sind. Ein paar bereits existierende Klassen könnten so nicht mehr direkt verwendet werden. Aber die Möglichkeit mit intrusive_ptr sieht grundsätzlich sehr interessant aus, könnte ich mir für später merken.

    Danke auch für all die anderen Hinweise, auf die ich jetzt nicht eingegangen bin.



  • Nexus schrieb:

    Shade Of Mine schrieb:

    oder der Manager laesst sich nicht beenden solange er noch Objekte managed.

    Wie würdest du das umsetzen, wenn man kein Singleton erzwingen möchte? Wäre mein vorhin erwähnter Ansatz des Frontend-Managers halbwegs sinnvoll, oder gibts andere bewährte Möglichkeiten?

    Prinzipiell sind Manager Klassen ja nicht da um sie zu erstellen und vernichten wie man lustig ist. zB koennte der Manager einfach eine Exception werfen wenn man ihn beenden will obwohl er noch Objekte besitzt. Alternativ kann er natuerlich auch brutal alle Objekte killen :p

    Andererseits ist die Frage: braucht man sowas ueberhaupt. Der Manager ist ja eher nur da um Resourcen zu beschaffen. Muss er von ihrer Vernichtung ueberhaupt in Kentniss gesetzt werden? Die Smartpointer koennen ja die Resourcen selber loeschen...

    Meine Hauptfrage ist eigentlich: was genau machen die Manager Klassen? Allokieren sie nur, passen sie auf dass eine Resource nur einmal geladen wird,... Was genau ist ihr Vorteil gegenueber einer einfachen acquire_resource() Funktion?



  • Shade Of Mine schrieb:

    zB koennte der Manager einfach eine Exception werfen wenn man ihn beenden will obwohl er noch Objekte besitzt.

    Dann dürfte der Manager sich im Destruktor aber nicht selbst beenden, weil sonst Exceptions während der Aufräumphase fliegen.

    Shade Of Mine schrieb:

    Meine Hauptfrage ist eigentlich: was genau machen die Manager Klassen? Allokieren sie nur, passen sie auf dass eine Resource nur einmal geladen wird,... Was genau ist ihr Vorteil gegenueber einer einfachen acquire_resource() Funktion?

    Der Manager soll schauen, ob eine bestimmte Ressource schon mal geladen wurde, und sie in diesem Fall nicht erneut laden. Im Prinzip also ein Cache.



  • Ich habe vorläufig folgendes Design implementiert:

    • ResourceManager gibt std::tr1::shared_ptr auf Ressourcen zurück (unter Verwendung eines eigenen Deleters)
    • Ressourcen leben solange es shared_ptr s auf sie gibt, kein Tracking der Referenzen mehr
    • ResourceManager kann aktiv shared_ptr auf Ressourcen halten, um Freigabe bei Nichtbenutzung zu verhindern
    • Ansonsten hält ResourceManager einen weak_ptr auf bereits allokierte Ressourcen, um bei erneuter Anfrage einen shared_ptr zu liefern

    Das ist soweit okay, vor allem wird die Verwendung eines standardisierten Smart-Pointers ermöglicht. Falls ich in Zukunft jedoch plötzlich etwas implementieren will, das mit shared_ptr nicht geht (z.B. Referenztracking, wenn auch nur für Debug-Zwecke), habe ich komplett verloren. Von daher bin ich mir nicht sicher, ob die Entfernung des ResourcePtr -Klassentemplates eine gute Entscheidung ist, oder ob sich die von Shade Of Mine vorgeschlagenen Custom Deleter im Endeffekt doch mehr lohnen. Was meint ihr?

    Kennt jemand Beispiele für ähnliche Ressourcenverwaltungssysteme, z.B. aus irgendeiner Game-Engine oder so?



  • War wohl nicht allzu klug, während einer Post-Flut zu antworten 😉

    Hätte jemand eine Idee hierzu? Oder Verweise auf ähnliche Designs?



  • Ich möchte den Thread jetzt nicht unnötig wieder ins Rampenlicht bringen, aber mich würde interessieren, ob Du "Beispiele für ähnliche Ressourcenverwaltungssysteme, z.B. aus irgendeiner Game-Engine oder so" gefunden hast.



  • Ich suchte damals noch ein wenig, aber die Ergebnisse waren nicht allzu brauchbar. Oft handelte es sich um allgemeine OOP-Designs à la Java, wo sowieso andere Probleme bestehen. Bei den C++-Implementierungen habe ich eigentlich nur Sachen gefunden, auf die man auch selbst kommt -- nur viel schlechter implementiert (oft mit rohen Zeigern und antikem Codestil). Ich habe aber auch nicht wahnsinnig lange gesucht, wahrscheinlich würde man schon was finden.



  • Alles klar, danke! 🙂


Anmelden zum Antworten