Ein Referenzzählmuster für speicherverwaltete Sprachen?


11

Java und .NET verfügen über wunderbare Garbage Collectors, die den Speicher für Sie verwalten, und über praktische Muster zum schnellen Freigeben externer Objekte ( Closeable, IDisposable), jedoch nur, wenn sie einem einzelnen Objekt gehören. In einigen Systemen muss eine Ressource möglicherweise unabhängig von zwei Komponenten verwendet und nur freigegeben werden, wenn beide Komponenten die Ressource freigeben.

In modernem C ++ würden Sie dieses Problem mit a lösen shared_ptr, wodurch die Ressource deterministisch freigegeben wird, wenn alle shared_ptrzerstört werden.

Gibt es dokumentierte, bewährte Muster für die Verwaltung und Freigabe teurer Ressourcen, die in objektorientierten, nicht deterministisch durch Müll gesammelten Systemen keinen einzigen Eigentümer haben?



1
@JoshCaswell Ja, und das würde das Problem lösen, aber ich arbeite in einem Müllsammelraum.
C. Ross

8
Die Referenzzählung ist eine Garbage Collection-Strategie.
Jörg W Mittag

Antworten:


15

Im Allgemeinen vermeiden Sie dies, indem Sie einen einzigen Eigentümer haben - auch in nicht verwalteten Sprachen.

Das Prinzip ist jedoch dasselbe für verwaltete Sprachen. Anstatt die teure Ressource auf einem sofort zu schließen Close(), dekrementieren Sie einen Zähler (erhöht auf Open()/ Connect()/ etc), bis Sie 0 erreichen. An diesem Punkt führt der Abschluss tatsächlich den Abschluss aus. Es wird wahrscheinlich wie das Fliegengewichtsmuster aussehen und sich so verhalten.


Das habe ich auch gedacht, aber gibt es ein dokumentiertes Muster dafür? Das Fliegengewicht ist sicherlich ähnlich, aber speziell für das Gedächtnis, wie es normalerweise definiert wird.
C. Ross

@ C. Ross Dies scheint ein Fall zu sein, in dem Finalisierer ermutigt werden. Sie können eine Wrapper-Klasse für die nicht verwaltete Ressource verwenden und dieser Klasse einen Finalizer hinzufügen, um die Ressource freizugeben. Sie können es auch implementieren lassen IDisposable, die Anzahl beibehalten, um die Ressource so schnell wie möglich freizugeben usw. Wahrscheinlich ist es oft das Beste, alle drei zu haben, aber der Finalizer ist wahrscheinlich der kritischste Teil, und die IDisposableImplementierung ist am wenigsten kritisch.
Panzercrisis

11
@Panzercrisis mit der Ausnahme, dass die Ausführung von Finalisierern nicht garantiert wird und insbesondere nicht garantiert, dass sie sofort ausgeführt werden .
Caleth

@ Caleth Ich dachte, die Sache mit den Zählungen würde bei der Schnelligkeit helfen. Meinst du, wenn sie überhaupt nicht laufen, meint die CLR möglicherweise nicht, bevor das Programm endet, oder meinst du, dass sie sofort disqualifiziert werden?
Panzercrisis


14

In einer durch Müll gesammelten Sprache (in der GC nicht deterministisch ist) ist es nicht möglich, die Bereinigung einer anderen Ressource als des Speichers zuverlässig an die Lebensdauer eines Objekts zu binden: Es kann nicht angegeben werden, wann ein Objekt gelöscht wird. Das Ende der Lebensdauer liegt ganz im Ermessen des Müllsammlers. Der GC garantiert nur, dass ein Objekt lebt, solange es erreichbar ist. Sobald ein Objekt nicht mehr erreichbar ist, wird es möglicherweise zu einem späteren Zeitpunkt bereinigt, was möglicherweise das Ausführen von Finalisierern umfasst.

Das Konzept des „Ressourcenbesitzes“ gilt in einer GC-Sprache nicht wirklich. Das GC-System besitzt alle Objekte.

Was diese Sprachen mit try-with-resource + Closeable (Java), mithilfe von Anweisungen + IDisposable (C #) oder mit Anweisungen + Kontextmanagern (Python) bieten, ist eine Möglichkeit für den Kontrollfluss (! = Objekte), eine Ressource zu speichern , die wird geschlossen, wenn der Kontrollfluss einen Bereich verlässt. In all diesen Fällen ähnelt dies einem automatisch eingefügten try { ... } finally { resource.close(); }. Die Lebensdauer des Objekts, das die Ressource darstellt, hängt nicht mit der Lebensdauer der Ressource zusammen: Das Objekt kann nach dem Schließen der Ressource weiterleben, und das Objekt ist möglicherweise nicht mehr erreichbar, solange die Ressource noch geöffnet ist.

Bei lokalen Variablen entsprechen diese Ansätze RAII, müssen jedoch explizit am Aufrufstandort verwendet werden (im Gegensatz zu C ++ - Destruktoren, die standardmäßig ausgeführt werden). Eine gute IDE warnt, wenn dies weggelassen wird.

Dies funktioniert nicht für Objekte, auf die von anderen Orten als lokalen Variablen verwiesen wird. Hier ist es unerheblich, ob es eine oder mehrere Referenzen gibt. Es ist möglich, die Ressourcenreferenzierung über Objektreferenzen über den Kontrollfluss in den Ressourcenbesitz zu übersetzen, indem ein separater Thread erstellt wird, der diese Ressource enthält. Auch Threads sind Ressourcen, die manuell verworfen werden müssen.

In einigen Fällen ist es möglich, den Besitz von Ressourcen an eine aufrufende Funktion zu delegieren. Anstelle von temporären Objekten, die auf Ressourcen verweisen, die zuverlässig bereinigt werden sollen (aber nicht können), enthält die aufrufende Funktion eine Reihe von Ressourcen, die bereinigt werden müssen. Dies funktioniert nur, bis die Lebensdauer eines dieser Objekte die Lebensdauer der Funktion überlebt und daher auf eine bereits geschlossene Ressource verweist. Dies kann von einem Compiler nur erkannt werden, wenn die Sprache über eine rostartige Eigentumsverfolgung verfügt (in diesem Fall gibt es bereits bessere Lösungen für dieses Ressourcenverwaltungsproblem).

Dies bleibt die einzig praktikable Lösung: manuelles Ressourcenmanagement, möglicherweise durch Implementierung der Referenzzählung. Dies ist fehleranfällig, aber nicht unmöglich. Insbesondere ist es in GC-Sprachen ungewöhnlich, über das Eigentum nachdenken zu müssen, sodass der vorhandene Code die Eigentumsgarantien möglicherweise nicht ausreichend explizit beschreibt.


3

Viele gute Informationen aus den anderen Antworten.

Um genau zu sein, suchen Sie möglicherweise nach kleinen Einzelobjekten für das RAII-ähnliche Kontrollflusskonstrukt über usingund IDisposein Verbindung mit einem (größeren, möglicherweise referenzgezählten) Objekt, das einige (operative) Objekte enthält System) Ressourcen.

Es gibt also die kleinen nicht gemeinsam genutzten Einzelbesitzerobjekte, die (über das kleinere Objekt IDisposeund das usingKontrollflusskonstrukt) wiederum das größere gemeinsam genutzte Objekt informieren können (möglicherweise benutzerdefiniert Acquireund ReleaseMethoden).

(Die Acquireund ReleaseMethoden unten sind dann auch außerhalb des Konstrukts verwendet, jedoch ohne die Sicherheit des tryimpliziten in using.)


Ein Beispiel in C #

void Test ( MyRefCountedClass myObj )
{
    using ( var usingRef = myObj.Acquire () )
    {
        var item = usingRef.Item;
        item.SomeMethod ();

        // the `using` automatically invokes Dispose() on usingRef
        //  which in turn invokes Release() on `myObj.
    }
}

interface IReferencable<T> where T: IReferencable<T> {
    Reference<T> Acquire ();
    void Release();
}

struct Reference<T>: IDisposable where T: IReferencable<T>
{
    public readonly T Item;
    public Reference(T item) { Item = item; _released = false; }
    public void Dispose() { if (! _released ) { _released = true; Item.Release(); } }
    private bool _released;
}

class MyRefCountedClass : IReferencable<MyRefCountedClass>
{
    private int _refCount = 0;

    public Reference<MyRefCountedClass> Acquire ()
    {
        _refCount++;
        return new Reference<MyRefCountedClass>(this);
    }

    public void Release ()
    {
        if (--_refCount <= 0)
            Dispose();
    }

    // NOTE that MyRefCountedClass does not have to implement IDisposable, but it can...
    // as shown here it doesn't implement the interface
    private void Dispose ()  
    {
        if ( _refCount > 0 )
            throw new Exception ("Dispose attempted on item in use.");
        // release other resources...
    }

    public int SomeMethod()
    {
        return 0;
    }
}

Wenn dies C # sein sollte (wie es aussieht), ist Ihre Reference <T> -Implementierung auf subtile Weise falsch. Der Vertrag für IDisposable.Disposebesagt, dass ein Disposemehrfacher Aufruf desselben Objekts ein No-Op sein muss. Wenn ich ein solches Muster implementieren würde, würde ich es auch Releaseprivat machen , um unnötige Fehler zu vermeiden und Delegierung anstelle von Vererbung zu verwenden (entfernen Sie die Schnittstelle, stellen Sie eine einfache SharedDisposableKlasse bereit, die mit beliebigen Einwegartikeln verwendet werden kann), aber das sind eher Geschmacksfragen.
Voo

@ Voo, ok, guter Punkt, danke!
Erik Eidt

1

Die überwiegende Mehrheit der Objekte in einem System sollte im Allgemeinen einem von drei Mustern entsprechen:

  1. Objekte, deren Zustand sich niemals ändern wird und auf die nur als Mittel zur Kapselung des Zustands verwiesen wird. Entitäten, die Verweise enthalten, wissen weder, noch kümmern sie sich darum, ob andere Entitäten Verweise auf dasselbe Objekt enthalten.

  2. Objekte, die unter der ausschließlichen Kontrolle einer einzelnen Entität stehen, die der alleinige Eigentümer aller darin enthaltenen Zustände ist und das Objekt lediglich als Mittel zur Kapselung des (möglicherweise veränderlichen) Zustands darin verwendet.

  3. Objekte, die einer einzelnen Entität gehören, die andere Entitäten jedoch nur eingeschränkt verwenden dürfen. Der Eigentümer des Objekts kann es nicht nur als Mittel zum Einkapseln des Status verwenden, sondern auch zum Einkapseln einer Beziehung zu den anderen Entitäten, die es gemeinsam nutzen.

Die Verfolgung der Speicherbereinigung funktioniert besser als die Referenzzählung für Nummer 1, da Code, der solche Objekte verwendet, nichts Besonderes tun muss, wenn er mit der letzten verbleibenden Referenz ausgeführt wird. Für # 2 ist keine Referenzzählung erforderlich, da Objekte genau einen Eigentümer haben und wissen, wann sie das Objekt nicht mehr benötigen. Szenario 3 kann einige Schwierigkeiten bereiten, wenn der Eigentümer eines Objekts es beendet, während andere Entitäten noch Referenzen besitzen. Selbst dort kann ein Tracking-GC besser als eine Referenzzählung sein, um sicherzustellen, dass Referenzen auf tote Objekte zuverlässig als Referenzen auf tote Objekte identifizierbar bleiben, solange solche Referenzen existieren.

Es gibt einige Situationen, in denen es erforderlich sein kann, dass ein gemeinsam nutzbares Objekt ohne Eigentümer externe Ressourcen erwirbt und hält, solange jemand seine Dienste benötigt, und diese freigeben sollte, wenn seine Dienste nicht mehr benötigt werden. Beispielsweise könnte ein Objekt, das den Inhalt einer schreibgeschützten Datei kapselt, von vielen Entitäten gleichzeitig freigegeben und verwendet werden, ohne dass eine von ihnen die Existenz der anderen kennen oder sich darum kümmern muss. Solche Umstände sind jedoch selten. Die meisten Objekte haben entweder einen einzigen eindeutigen Eigentümer oder sind ohne Eigentümer. Mehrfachbesitz ist möglich, aber selten sinnvoll.


0

Geteiltes Eigentum macht selten Sinn

Diese Antwort mag etwas tangential sein, aber ich muss fragen, in wie vielen Fällen ist es aus Anwendersicht sinnvoll, das Eigentum zu teilen ? Zumindest in den Domänen, in denen ich gearbeitet habe, gab es praktisch keine, da dies andernfalls bedeuten würde, dass der Benutzer nicht einfach einmal etwas von einem Ort entfernen muss, sondern es explizit von allen relevanten Eigentümern entfernen muss, bevor die Ressource tatsächlich vorhanden ist aus dem System entfernt.

Es ist oft eine untergeordnete technische Idee, um zu verhindern, dass Ressourcen zerstört werden, während noch etwas anderes darauf zugreift, wie ein anderer Thread. Wenn ein Benutzer etwas aus der Software schließen / entfernen / löschen möchte, sollte es häufig so schnell wie möglich entfernt werden (wann immer es sicher zu entfernen ist), und es sollte auf keinen Fall so lange verweilen und ein Ressourcenleck verursachen Die Anwendung wird ausgeführt.

Beispielsweise kann ein Spielelement in einem Videospiel auf ein Material aus der Materialbibliothek verweisen. Wir wollen sicher keinen Absturz eines baumelnden Zeigers, wenn das Material in einem Thread aus der Materialbibliothek entfernt wird, während ein anderer Thread noch auf das Material zugreift, auf das das Spiel-Asset verweist. Dies bedeutet jedoch nicht, dass es für Spiel-Assets sinnvoll ist, das Eigentum an Materialien, auf die sie verweisen, mit der Materialbibliothek zu teilen . Wir möchten den Benutzer nicht zwingen, das Material explizit aus dem Asset und der Materialbibliothek zu entfernen. Wir möchten nur sicherstellen, dass Materialien nicht aus der Materialbibliothek, dem einzigen vernünftigen Eigentümer von Materialien, entfernt werden, bis andere Fäden auf das Material zugegriffen haben.

Ressourcenlecks

Ich habe jedoch mit einem ehemaligen Team zusammengearbeitet, das GC für alle Komponenten der Software übernommen hat. Und während dies wirklich dazu beitrug, sicherzustellen, dass niemals Ressourcen zerstört wurden, während andere Threads noch auf sie zugegriffen haben, haben wir stattdessen unseren Anteil an Ressourcenlecks erhalten .

Und dies waren keine trivialen Ressourcenlecks, die nur Entwickler verärgern, wie ein Kilobyte Speicher, der nach einer einstündigen Sitzung verloren gegangen ist. Dies waren epische Lecks, oft Gigabyte Speicher während einer aktiven Sitzung, die zu Fehlerberichten führten. Denn wenn jetzt auf den Besitz einer Ressource verwiesen wird (und daher im Besitz von beispielsweise 8 verschiedenen Teilen des Systems geteilt wird), ist nur einer erforderlich, um die Ressource nicht zu entfernen, wenn der Benutzer anfordert, sie zu entfernen durchgesickert und möglicherweise auf unbestimmte Zeit.

Ich war also nie ein großer Fan von GC oder Referenzzählungen, die in großem Maßstab angewendet wurden, weil sie es einfach machten, undichte Software zu erstellen. Was früher ein baumelnder Zeigerabsturz gewesen wäre, der leicht zu erkennen ist, wird zu einem sehr schwer zu erkennenden Ressourcenleck, das leicht unter dem Radar der Tests fliegen kann.

Schwache Referenzen können dieses Problem mindern, wenn die Sprache / Bibliothek diese bereitstellt. Ich fand es jedoch schwierig, ein Team von Entwicklern mit gemischten Fähigkeiten dazu zu bringen, schwache Referenzen bei Bedarf konsistent zu verwenden. Und diese Schwierigkeit hing nicht nur mit dem internen Team zusammen, sondern mit jedem einzelnen Plugin-Entwickler für unsere Software. Auch sie können leicht dazu führen, dass das System Ressourcen verliert, indem sie lediglich einen dauerhaften Verweis auf ein Objekt auf eine Weise speichern, die es schwierig macht, auf das Plugin als Schuldigen zurückzugreifen. Daher haben wir auch den Löwenanteil der Fehlerberichte erhalten, die sich aus unseren Softwareressourcen ergeben Es wurde einfach durchgesickert, weil ein Plugin, dessen Quellcode außerhalb unserer Kontrolle lag, keine Verweise auf diese teuren Ressourcen freigeben konnte.

Lösung: Aufgeschobene, regelmäßige Entfernung

Meine Lösung, die ich später auf meine persönlichen Projekte anwendete, die mir das Beste aus beiden Welten gaben, bestand darin, das Konzept zu beseitigen, das referencing=ownershipdie Zerstörung von Ressourcen verzögert hat.

Infolgedessen wird die API immer dann ausgedrückt, wenn der Benutzer etwas tut, das bewirkt, dass eine Ressource entfernt werden muss, indem nur die Ressource entfernt wird:

ecs->remove(component);

... die die Benutzerendlogik auf sehr einfache Weise modelliert. Die Ressource (Komponente) wird jedoch möglicherweise nicht sofort entfernt, wenn sich andere Systemthreads in ihrer Verarbeitungsphase befinden, in denen sie möglicherweise gleichzeitig auf dieselbe Komponente zugreifen.

Diese Verarbeitungsthreads geben dann hier und da Zeit, wodurch ein Thread, der einem Garbage Collector ähnelt, aufwacht und " die Welt stoppt " und alle Ressourcen zerstört, deren Entfernung angefordert wurde, während Threads von der Verarbeitung dieser Komponenten ausgeschlossen werden, bis sie fertig sind . Ich habe dies so eingestellt, dass der Arbeitsaufwand hier im Allgemeinen minimal ist und sich nicht merklich auf die Bildraten auswirkt.

Jetzt kann ich nicht sagen, dass dies eine bewährte und gut dokumentierte Methode ist, aber ich verwende sie seit einigen Jahren ohne Kopfschmerzen und ohne Ressourcenlecks. Ich empfehle, solche Ansätze zu untersuchen, wenn Ihre Architektur für diese Art von Parallelitätsmodell geeignet ist, da es viel weniger schwerfällig ist als GC oder Ref-Counting und diese Art von Ressourcenlecks nicht riskiert, wenn sie unter dem Radar des Testens fliegen.

Der einzige Ort, an dem ich Ref-Counting oder GC als nützlich empfand, ist für persistente Datenstrukturen. In diesem Fall handelt es sich um ein Datenstrukturgebiet, das weit entfernt von Bedenken des Benutzers ist, und dort ist es tatsächlich sinnvoll, dass jede unveränderliche Kopie möglicherweise das Eigentum an denselben unveränderten Daten teilt.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.