Wie kann ein Ressourcenmanager dynamisch laden / entladen, ohne baumelnde Zeiger zu erstellen?

7

Ich habe einen Ressourcenmanager, der die Lebensdauer der Ressourcen im Speicher verwaltet. Eine "Ressource" ist so etwas wie eine Textur, ein Netz usw. Ich habe auch eine Entitätsklasse, die Basisklasse für Dinge in meiner Spielwelt, die Zeiger auf einzelne Ressourcen enthält.

Ich denke darüber nach, meinem Ressourcenmanager ein Speicherkontingent zu geben, damit er Ressourcen nach Bedarf freigeben und neu laden kann, um dieses Kontingent zu erfüllen, und ich frage mich, wie ich dies tun kann, ohne dass in meiner Entität baumelnde Zeiger entstehen Objekte.

Ich benötige die Möglichkeit, jederzeit im Ressourcenmanager ein Netz oder eine Textur freizugeben.

Ich könnte es shared_ptrim Ressourcenmanager und weak_ptrin den Entitäten verwenden (solange ich keine shared_ptrAußenseite des Ressourcenmanagers verfügbar mache , kann ich sie bei Bedarf freigeben), aber ich bin mehr daran interessiert, wie Sie würde es ohne diese Klassen tun, zum Beispiel bevor C ++ 11 existierte oder bevor die Smart-Pointer-Bibliothek von Boost existierte.

EddieV223
quelle

Antworten:

7

Halten Sie statt roher Zeiger einen anderen einfachen Verweis auf die Ressource. A shared_ptrund entsprechende weak_ptrInstanzen sind ein solcher Mechanismus, aber Sie können Ihren eigenen einfach implementieren, wenn Sie keinen Zugriff auf diese Typen (oder ihre früheren Boost-Äquivalente) haben oder wenn es andere Gründe gibt, warum diese Typen für Ihr Szenario nicht geeignet wären (z als zu schwer).

Diese Art von 'Handle'-System kann so einfach sein wie das Verteilen von Ganzzahlen, die sich auf interne Slots im Ressourcensystem beziehen. Diese können dann verwendet werden, um eine Ressource dynamisch neu zu laden, wenn während des Entladens Zugriff darauf angefordert wird.

In der Praxis möchten Sie wahrscheinlich etwas Robusteres, zumindest eine Möglichkeit, das Handle "wie" einen Zeiger zu behandeln und operator ->Überladungen bereitzustellen , indem Sie es beispielsweise in einen HandleTyp einschließen . Dieser HandleTyp kann nur einen Zeiger auf einen im Ressourcenmanager gespeicherten "Steuerblock" enthalten. Der Steuerblock enthält die Rohressourcenreferenz und ein boolesches Flag, das angibt, ob die Ressource aktiv ist oder nicht, sowie eine Art Schlüssel, mit dem die Ressource bei Bedarf neu geladen werden kann (im folgenden Beispiel wird eine Zeichenfolge angenommen). Ein sehr einfaches Beispiel hierfür könnte folgendermaßen aussehen:

template <typename Resource>
struct Handle {
    Resource * operator -> () }
        return m_owner->retrieve_resource(m_resource_id);
    }

private:
    int m_resource_id;
    resource_manager * m_owner;
};

namespace detail {
    struct control_block {
      Resource * resource;
      bool is_alive;
      std::string key;
    };
};

struct resource_manager {
    Resource * retrieve_resource (int id) {
        auto block = m_resources[id];
        if (block->is_alive) {
          return block->resource;
        } else {
          block->resource = reload_resource(key);
          block->is_alive = true;
          return block->resource;
        }
    }

private:
    std::vector<detail::control_block> m_resources;
};

In dieser Implementierung speichert der Ressourcenmanager nur ein flaches Array von Steuerblöcken, auf die durch eine Ganzzahl verwiesen werden kann, und bietet eine Methode zum Wiederherstellen eines Ressourcenzeigers anhand der ID, wobei diese Ressource bei Bedarf neu geladen wird. Die HandleKlasse ist nur ein nützlicher Wrapper für dieses Verhalten. Offensichtlich ist dies jedoch ein sehr einfaches Beispiel. Wenn Sie etwas vollständigeres wünschen, lesen Sie den Artikel von Scott Bilas über einen generischen handle-basierten Ressourcenmanager . Es geht etwas anders vor, ist aber die Lektüre wert.

(Seien Sie gewarnt, dass ein Bedarf, den Ihre Frage impliziert, bedeuten kann, dass Ihre Ressourcennutzung sehr chaotisch ist, und das ist nicht unbedingt eine gute Sache für die Leistung, daher sollten Sie dies in Betracht ziehen.)


quelle
8

Intelligente Zeiger oder Handle-Klassen, genau wie C ++ 11. Sie könnten lange vor C ++ 11 Ihre eigenen schreiben. Ich würde sogar argumentieren, dass das shared_ptrin C ++ 11 wahrscheinlich das völlig falsche Modell ist.

Sie erstellen lediglich einen Typ, der einen Verweis auf eine Ressource enthält. Diese Referenz kann ein Zeiger mit einer Art Lebensdauerverwaltung sein oder es kann sich nur um eine Art numerischen Bezeichner handeln, der als Schlüssel für eine Datenstruktur verwendet wird (Array-Index, Kartenschlüssel, was auch immer). Verwenden Sie dies, um auf Ressourcen zuzugreifen. Ähnlich wie es weak_ptrfunktioniert, außer dass Sie sich keine Gedanken darüber machen sollten, sie mit einer shared_ptrersten zu "sperren" . Ressourcen sollten immer erst freigegeben werden, nachdem die gesamte Logik und das Rendern für den Frame abgeschlossen sind und keine Möglichkeit besteht, dass Zeiger hängen bleiben (vorausgesetzt, Sie verwenden die entsprechenden Handles überall in Ihren nicht temporären Datenstrukturen).

Beachten Sie, dass es eine schlechte Idee ist, Ressourcen nur zufällig auszulagern. Wenn ein Objekt existiert und sagt, dass es eine Textur benötigt , benötigt es diese Textur. Wenn Ihnen der Speicher ausgeht, zielen Sie entweder (a) auf eine bessere Hardware ab, (b) machen Sie ein kleineres Spiel oder (c) implementieren Sie ein ausgefeilteres Ressourcenverwaltungssystem, mit dem Sie den aktuellen Arbeitssatz und die geeigneten Zeiten für Objekte genauer bestimmen können um ihre Ressourcen freizugeben oder wieder zu beschaffen (unter Verwendung des Detaillierungsgrades, des Interessenbereichs usw.).

Sean Middleditch
quelle
Können Sie erläutern, warum C ++ 11 shared_ptr das falsche Modell ist? auch mit make_shared? liegt es an der Leistung oder an der Verwendung von Pattern / Syntax?
Konzept3d
@ concept3d: Ich habe an einem Artikel darüber gearbeitet; es ist schwer zusammenzufassen. Am Ende ist es nur ein schlechtes Eigentumsmodell. Nur ein Objekt sollte jemals ein anderes besitzen; Alle anderen sollten "Abonnenten" sein, die verlangen, dass der Eigentümer sie behält, und die nachvollziehbar / debuggbar sein sollten (Sie sollten jederzeit in der Lage sein, alle aktiven Handles für eine Ressource aufzulisten, mit denen Sie nichts anfangen können shared_ptr). Es hat alle Fehler der automatischen GC in Bezug auf die Semantik und alle Nachteile der Referenzzählung in Bezug auf die Implementierung.
Sean Middleditch
gut, ich verstehe. Ich habe darüber nachgedacht, meine Ressourcenmanager mithilfe von shared_ptrs zu implementieren, aber die Dinge werden klarer, wenn ich Ihren Artikel lese. Ist er fertig?
Konzept3d
@ concept3d: vielleicht. Ich würde nicht damit rechnen, dass es bald fertig ist. Ich kann im Moment nur aus einigen Jahrzehnten kollektiver Erfahrung zwischen mir und einigen führenden Motorenarchitekten, mit denen ich in letzter Zeit gesprochen habe, sagen, dass shared_ptrintelligente Zeiger tendenziell mehr Kopfschmerzen verursachen, als sie wert sind. Eine Menge von sehr schwer zu debuggen Problemen kann auftauchen Nähe des Versands beginnen , die alle laufen auf Referenz Lecks (aber kein Clearing shared_ptrwenn Sie sollen) oder Destruktoren zur falschen Zeit aufgerufen werden (Objekte am Leben zu lange bleiben dann bei einer Zerstörung schlechte Zeit).
Sean Middleditch
Vielen Dank für die Details, nachdem Sie gründlich darüber nachgedacht haben, was Sie gesagt haben, ist es absolut sinnvoll. Es sollte nur einen Eigentümer und andere Abonnenten geben (oder etwas, das schwach_ptr ähnlich ist). +1.
Konzept3d