Ressourcenverwaltung (besonders Freigabe)



  • Hallo zusammen,

    Ich schreibe mir einen kleinen Ressourcen-Manager, der bisher folgende Features unterstützt:

    • Bei Anfrage über Acquire() Rückgabe der entsprechenden Ressource, falls nötig mit automatischem Laden
    • Zugriff auf Ressourcen über geteilten Smart-Pointer ResourcePtr , der immer gültig oder Null ist
    • Automatische Freigabe, wenn Ressource nicht mehr referenziert wird (standardmässig wird aber gecacht und explizit freigegeben)

    Frage 1: Nun überlege ich mir, eine Release() -Methode anzubieten, mit der einzelne Ressourcen über eine ID freigegeben werden können. Was ist eurer Meinung nach sinnvoller?

    1. Nur freigeben, falls Ressource gerade nicht benutzt wird, d.h. von keinem ResourcePtr referenziert wird.
    2. Immer freigeben, und dabei alle benutzenden ResourcePtr s auf NULL setzen. Kann es sinnvoll sein, den Benutzern die Ressourcen zu entziehen?

    Frage 2: Beim Freigeben können Fehler auftreten, entweder weil die ID ungültig ist oder weil die Ressource im Falle a) noch verwendet wird. Ich würde am ehesten eine Exception werfen, gibt es hier Gründe für eine andere Fehlerbehandlung (z.B. bool zurückgeben)?



  • Nun überlege ich mir, eine Release()-Methode anzubieten, mit der einzelne Ressourcen über eine ID freigegeben werden können.

    Welchen Nutzen würde so eine Release()-Methode haben? Besondern dann, wenn du sowieso auf Smart-Pointer setzt, die selber wissen wann sie ihre Ressourcen freigegeben können.



  • Im Ressourcen-Manager gibt es ein Flag AutoRelease. Wenn das aktiviert ist, werden Ressourcen so schnell wie möglich wieder freigegeben (d.h. sobald der Referenzzähler des Smart-Pointers Null erreicht). Andererseits kann ich mir gut vorstellen, dass es manchmal erwünscht ist, dass eine Ressource im Speicher gehalten wird, auch wenn sie temporär unbenutzt bleibt.

    Bei diesem Caching (AutoRelease deaktiviert) möchte man vielleicht dennoch einzelne Ressourcen wieder freigeben, daher brauchts sowas wie ein explizites Release() . Oder siehst du das anders?



  • Ah, okay.

    In dem Fall würde ich auf Vorgehensweise 1a bevorzugen, da es mir als Benutzer die Verantwortung dafür abnehmen würde, darauf zu achten, dass ich auch nichts vorzeitig wieder freigebe (und dadurch irgendwelche Bilder nicht gezeichnet werden können).
    Wobei ich hierbei auch keine Exception (oder Error Code) werfen würde, wenn die Ressource noch verwendet wird, sondern Release() eher als eine Funktion ansehen würde, die das AutoRelease Flag für diese eine Ressource wieder setzt. Ich gebe die Ressource also wieder zur automatischen Verwaltung frei.

    Man könnte allerdings auch argumentieren, dass jemand, der sowieso selber die Dateien freigeben möchte (z.B. über Release() ), auch die volle Kontrolle über die Ressourcen haben möchte (also 1b). Diese erhöhte Kontrolle ist natürlich mit erhöhtem Aufwand auf Seiten des Nutzers verbunden.
    Allerdings finde ich, dass so etwas den Sinn einer Ressourcenverwaltung etwas ad absurdum führt. Für solche Fälle könnte man evtl. stattdessen die Möglichkeit vorsehen die Ressource komplett dem Benutzer zu übergeben und dem Ressourcenmanager somit zu entziehen (der umgekehrte Weg sollte natürlich auch möglich sein).



  • Meine Befürchtung bei 1b) ist eben auch, dass bei einem "Enforce-Release" plötzlich Ressourcenbenutzer unvorbereitet vor dem Nichts stehen und seelenruhig Nullzeiger dereferenzieren. Andererseits hab ich mir auch das mit der Kontrolle überlegt – mit dem Hintergedanken, dass eine Art Resource Leak entstehen kann, wenn ein ResourcePtr irgendwo nicht mehr freigegeben wird. Aber das ist dann wohl eher ein Benutzerfehler, den man nicht speziell berücksichtigen sollte. Die Entscheidung läuft darauf hinaus, ob ResourcePtr eine starke oder schwache Referenz darstellt. Kann sich jemand sonst einen gerechtfertigten Fall für Ressourcen-Entziehen (also Weak-References) vorstellen?

    Dass Release() das AutoRelease-Flag für die Ressource automatisch setzt, ist eine interessante Idee (ganz nach dem Prinzip "wenn du schon jetzt nicht freigeben kannst, tu das wenigstens so schnell wie möglich"). Momentan gibt es nur ein Flag für den ganzen Ressourcen-Manager, aber es könnte sich vielleicht lohnen, dieses pro Ressource zu setzen.

    Danke vielmals für die Antwort!


  • Mod

    resourcen haben oft an sich dass dem user der inhalt nicht wirklich wichtig ist, er nur das resourcen objekt verwaltet, dabei bietet es sich also an proxy resourcen zu erstellen die auf richtige resourcen verweisen.
    dabei kannst du dann
    - ein proxy zurueckgeben ohne dass die resource ueberhaupt schon da ist (z.B. weil das streaming noch nicht fertig ist)
    - du kannst eine dummy resource referenzieren (z.b. weil eine resource ueberhaupt nicht vorhanden ist, weil vielleicht jemand dir ein level gegeben hat und du noch nicht z.b. die neusten texturen hast)
    - du kannst resourcen abaendern (z.b. wird der speicher knapp und eine weniger wichtige 'specular map' wird um 2 mipmaps reduziert damit eine 'diffuse map' geladen werden kann).

    wenn du wirklich nur acquire und release haettest, was nur refcounted, dann braeuchtest du dir nicht die muehe eines resource managers machen, da wuerde eine simple factory und recounting ausreichen, das kannst du dann fuer mehr als 'resources' benutzen.
    willst du wirklich resources haben, hat niemand einen pointer auf die eigentliche resource, nur auf das verwaltungsobjekt. um zugriff auf die resource zu erhalten, bietest du dann entweder map/unmap und/oder read/write funktionen an.



  • Die Idee mit Proxy-Ressourcen klingt gut, doch ich frage mich, ob mir die Indirektion momentan so viel bringt. Mein Ressourcen-Manager ist zur Zeit nicht viel mehr als ein Wrapper um std::map mit sicherem Zugriff über die Smart-Pointers und automatischem Laden (siehe Punkte im ersten Post). Ich habe mir auch überlegt, in Zukunft Multithreading einzusetzen, um z.B. nicht-blockierend laden zu können. Dann würden Proxies immer mehr an Bedeutung erlangen. Den Vorschlag werde ich mir merken, danke!

    Da ich mit SFML arbeite, geht es mir vorerst hauptsächlich um Ressourcen wie Bilder, Schriftarten oder Sounds. Dabei gibt es schon eine Indirektion (z.B. durch Sprites, die während ihrer ganzen Lebenszeit auf Bilder verweisen). Ich habe das Gefühl, dass Ressourcen nicht gross herumgereicht werden, bevor man auf sie zugreift, und dass ein separates Read/Write den Status Quo nur verkomplizieren würde. Aber gut möglich, dass ich später alles noch flexibler mache.



  • Wie würdet ihr ausserdem darauf reagieren, wenn Release() eine ungültige Ressourcen-ID übergeben wird?

    Meiner Ansicht nach ist das Freigeben nicht vorhandener Ressourcen ein Logikfehler, der bei korrektem Code nie vorkommt. Daher tendiere ich zu assert .


  • Mod

    ich wuerde schon dazu tendieren bei einer falschen ID ein debugbreak zu setzen bzw exception zu werfen.
    Wenn man die IDs in einer klasse kapselt, sollte das eigentlich NIE passieren, wenn es dennoch passieren sollte, ist entweder etwas komplett schief gelaufen (z.B. race durch multithreading), es gibt einen wilden pointer oder sonst ein seltsammer grund (was man also wirklich nicht erwartet).
    Dabei kann man garnicht strikt genug dagegen vorgehen, denn genauso gut wie eine falsche ID koennte es sein, dass an einer anderen stelle eine ID verwendet wird die schon ein anderes objekt representiert (sprich: etwas wird freigegeben, an die stelle wird was neues allokiert und dann kommt die ID fuer das schon freigegebene objekt, das bleibt unbemerkt).

    wenn es geht, IDs vermeiden und auf objekten arbeiten, wuerde ich sagen :).



  • Okay, danke für die Bestätigung. Ja, es handelt sich momentan nicht nur um eine Integer-ID, sondern ein Objekt, das die spezifischen Ressourceneigenschaften kapselt (und Ressourcen eindeutig voneinander unterscheiden kann).

    Und nun stehe ich schon wieder vor der nächsten Frage: Soll die Release-Strategie (AutoRelease oder ExplicitRelease, siehe oben) im Konstruktor festgelegt werden oder würdet ihr es sinnvoll finden, diese nachträglich zu ändern? Da das Flag pro Ressource existiert, müsste ich auch festlegen, ob bei einer Änderung alle Ressourcenflags geändert werden, oder nur solche, die ab jetzt geladen werden...

    Wahrscheinlich denke ich aber wiedermal zu kompliziert 🙂



  • Ich werde nachträgliche Strategie-Änderungen vorerst zulassen. Am flexibelsten bin ich wohl mit Callbacks über std::tr1::function , sodass der Benutzer auch seine eigenen Freigabe- und Ladefehler-Strategien angeben kann.

    Jetzt muss ich "nur" noch einen Weg finden, das einigermassen sauber unter einen Hut zu bringen und die Semantik möglichst intuitiv zu gestalten...



  • ad 1)
    Ganz klar a: Resourcen auf die noch ein Shared-Pointer exisitert nicht freigeben. Grund: würde man die Zeiger einfach NULL machen, müssten alle Programmteile die mit solchen Zeigern hantieren damit klarkommen, wenn diese einfach mal so NULL werden. Speziell im Zusammenhang mit Multithreading sehr problematisch, aber auch ohne wohl zumindest sehr lästig.

    ad 2)
    Ich persönlich würde keine Exceptions werfen, und zwar weil das Freigeben von Dingen immer "no-throw" sein sollte. U.a. damit man es in Destruktoren erledigen kann. Den "ungültige ID" Fall würde ich mit einem ASSERT() behandeln und den "wird noch gebraucht" Fall würde ich vermutlich einfach explizit erlauben. In beiden Fällen sollte der Release-Code einfach nichts tun. Oder aber bei "ungültige ID" ne Logausgabe schreiben und das Programm sofort anhalten. Auf jeden Fall nicht Exception werfen und weiterlaufen lassen.



  • hustbaer schrieb:

    Grund: würde man die Zeiger einfach NULL machen, müssten alle Programmteile die mit solchen Zeigern hantieren damit klarkommen, wenn diese einfach mal so NULL werden. Speziell im Zusammenhang mit Multithreading sehr problematisch, aber auch ohne wohl zumindest sehr lästig.
    [...]
    Ich persönlich würde keine Exceptions werfen, und zwar weil das Freigeben von Dingen immer "no-throw" sein sollte.

    Das sind gute Argumente, vielen Dank!

    hustbaer schrieb:

    Den "ungültige ID" Fall würde ich mit einem ASSERT() behandeln und den "wird noch gebraucht" Fall würde ich vermutlich einfach explizit erlauben.

    So mach ichs jetzt gerade. Ein bool -Rückgabetyp könnte vielleicht nützlich sein, damit der Aufrufer überprüfen kann, ob seine Freigabe geklappt hat.

    • Ungültige ID -> Assertion bricht ab
    • Gültige ID, aber noch benutzt -> return false
    • Gültige ID, kann freigegeben werden -> return true

    Allerdings könnte ich evtl. auch eine zusätzliche Methode bool IsInUse(ID id) schreiben, um das explizit zu überprüfen. Aber demnächst bleibe ich wohl beim Alten.

    Was die Strategien betrifft: Ich tendiere momentan zu einfachen enum s, bis ich ein gutes Design entwickelt habe, das benutzerdefinierte Reaktionen ermöglicht. Ist halt noch nicht so flexibel, aber sollte für viele Fälle reichen und ist einfach zu verstehen.



  • Ja, ein bool als Returnwert kann in dem Fall nicht schaden. Bzw. ist sogar guter Stil.
    Informationen an die man bei der Ausführung einer Funktion "gratis" drankommt, und die für den Aufrufer von Nutzen sein könnten, sollte man auch zurückgeben.

    Wobei man natürlich immer den potentiellen Nutzen gegen die Kosten abwägen muss, aber bei einer Funktion die sonst keine Werte zurückgibt, sind die Kosten nen bool zurückzugeben quasi nicht-existent.



  • hustbaer schrieb:

    Informationen an die man bei der Ausführung einer Funktion "gratis" drankommt, und die für den Aufrufer von Nutzen sein könnten, sollte man auch zurückgeben.

    Genau, das war mein Hintergedanke. Hat das nicht mal Alexander Stepanov oder so gesagt?



  • Jopp, das erwähnt er in einem seiner Papers 🙂


Anmelden zum Antworten