Wann sollten schwache Referenzen in .Net verwendet werden?

56

Ich persönlich bin nicht auf eine Situation gestoßen, in der ich den Typ WeakReference in .Net verwenden musste, aber die weit verbreitete Überzeugung scheint zu sein, dass er in Caches verwendet werden sollte. In seiner Antwort auf diese Frage sprach sich Dr. Jon Harrop sehr gut gegen die Verwendung von WeakReferences in Caches aus .

Ich habe auch oft gehört, wie AS3-Entwickler davon gesprochen haben, schwache Referenzen zu verwenden, um Speicherplatz zu sparen, aber aufgrund der Gespräche, die ich geführt habe, scheint dies die Komplexität zu erhöhen, ohne das beabsichtigte Ziel zu erreichen, und das Laufzeitverhalten ist ziemlich unvorhersehbar. So sehr, dass viele einfach aufgeben und stattdessen die Speichernutzung sorgfältiger verwalten / ihren Code so optimieren, dass er weniger speicherintensiv ist (oder den Kompromiss aus mehr CPU-Zyklen und geringerem Speicherbedarf eingeht).

Dr. Jon Harrop wies in seiner Antwort auch darauf hin, dass die .Net-Schwachstellen nicht weich sind und es bei gen0 eine aggressive Sammlung von Schwachstellen gibt. Laut MSDN , langer schwacher Verweise gibt Ihnen die Möglichkeit , ein Objekt zu erstellen, but the state of the object remains unpredictable.!

Angesichts dieser Eigenschaften kann ich mir keine Situation vorstellen, in der schwache Referenzen nützlich wären, vielleicht könnte mich jemand aufklären?

theburningmonk
quelle
3
Sie haben bereits mögliche Verwendungszwecke für dieses Produkt skizziert. Natürlich gibt es andere Möglichkeiten, um sich diesen Situationen zu nähern, aber es gibt mehr als eine Möglichkeit, eine Katze zu häuten. Wenn Sie nach einem kugelsicheren "Sie sollten immer eine WeakReference verwenden, wenn X" suchen, bezweifle ich, dass Sie eine finden werden.
2
@ itsme86 - Ich suche keinen kugelsicheren Anwendungsfall, nur solche, für die schwache Referenzen gut geeignet und sinnvoll sind. Der Cache-Anwendungsfall zum Beispiel, weil schwache Referenzen so eifrig gesammelt werden, dass es einfach zu mehr Cache-Fehlern kommt, selbst wenn Sie über genügend Arbeitsspeicher verfügen
4
Ich bin ein wenig enttäuscht, dass dies einige Stimmen zum Abschluss bringt. Ich hätte nichts dagegen, eine Antwort oder eine Diskussion darüber zu sehen (in b4 "Stack Overflow ist kein Forum").
ta.speot.is
@theburningmonk Dies ist der Offset für die Speichergewinnung. Im heutigen Rahmen ist es zweifelhaft, ob jemand direkt zum WeakReference-Tool greifen würde, selbst wenn ein Cache implementiert wird, da umfangreiche Cachesysteme verfügbar sind.
Hier ist ein peinlich überkompliziertes Beispiel für deren Verwendung (für das Schwachereignismuster, das ta.speot.is unten beschreibt)
Benjol,

Antworten:

39

Ich habe in den folgenden drei realen Szenarien, die mir persönlich tatsächlich passiert sind, legitime praktische Anwendungen von schwachen Referenzen gefunden:

Anwendung 1: Eventhandler

Du bist ein Unternehmer. Ihr Unternehmen vertreibt eine Funkenstreckensteuerung für WPF. Der Verkauf ist großartig, aber die Supportkosten bringen Sie um. Zu viele Kunden beklagen sich über CPU-Überlastung und Speicherverluste, wenn sie durch mit Funkenlinien gefüllte Bildschirme scrollen. Das Problem ist, dass ihre App neue Funkenlinien erstellt, wenn sie angezeigt werden, aber die Datenbindung verhindert, dass die alten mit Müll eingesammelt werden. Wie geht's?

Fügen Sie einen schwachen Verweis zwischen der Datenbindung und Ihrem Steuerelement ein, damit die Datenbindung allein nicht mehr verhindert, dass Ihr Steuerelement durch Müll gesammelt wird. Fügen Sie Ihrem Steuerelement dann einen Finalizer hinzu, der die Datenbindung aufhebt, wenn sie erfasst wird.

Anwendung 2: Veränderbare Graphen

Du bist der nächste John Carmack. Sie haben eine geniale neue grafische Darstellung hierarchischer Unterteilungsoberflächen erfunden, mit der Tim Sweeneys Spiele wie eine Nintendo Wii aussehen. Natürlich werde ich Ihnen nicht genau sagen , wie es funktioniert, aber es dreht sich alles um diesen veränderlichen Graphen, in dem sich die Nachbarn eines Scheitelpunkts in a befinden Dictionary<Vertex, SortedSet<Vertex>>. Die Topologie des Diagramms ändert sich ständig, während der Player herumläuft. Es gibt nur ein Problem: Ihre Datenstruktur wirft während der Ausführung nicht erreichbare Untergraphen ab und Sie müssen sie entfernen, da sonst Speicherplatz verloren geht. Glücklicherweise sind Sie ein Genie und wissen, dass es eine Klasse von Algorithmen gibt, die speziell dafür entwickelt wurden, unerreichbare Untergraphen zu lokalisieren und zu sammeln: Müllsammler! Sie haben die ausgezeichnete Monographie von Richard Jones zu diesem Thema gelesenAber Sie sind verwirrt und besorgt über Ihre bevorstehende Frist. Wie geht's?

Indem Sie einfach Ihre Dictionarydurch eine schwache Hash-Tabelle ersetzen , können Sie den vorhandenen GC huckepack nehmen und Ihre nicht erreichbaren Subgraphen automatisch für Sie sammeln lassen! Zurück zu Ferrari-Anzeigen.

Anwendung 3: Bäume schmücken

Sie hängen an der Decke eines zyklindrischen Raumes an einer Tastatur. Sie haben 60 Sekunden Zeit, um einige GROSSE DATEN zu sichten, bevor Sie jemand findet. Sie haben einen schönen Parser auf Stream-Basis erhalten, der sich auf die GC stützt, um AST-Fragmente zu sammeln, nachdem sie analysiert wurden. Sie stellen jedoch fest, dass Sie zusätzliche Metadaten für jeden AST Nodebenötigen und dass Sie diese schnell benötigen. Wie geht's?

Sie könnten a verwenden Dictionary<Node, Metadata>, um jedem Knoten Metadaten zuzuordnen, aber die starken Verweise aus dem Wörterbuch auf alte AST-Knoten halten sie am Leben und führen zu Speicherverlusten. Die Lösung ist eine schwache Hash-Tabelle , die nur schwache Verweise auf Schlüssel enthält, und Garbage sammelt Schlüsselwertbindungen, wenn der Schlüssel nicht mehr erreichbar ist. Wenn AST-Knoten nicht mehr erreichbar sind, werden sie nicht mehr erfasst, und ihre Schlüsselwertbindung wird aus dem Wörterbuch entfernt. Die entsprechenden Metadaten sind dann nicht mehr erreichbar, sodass sie ebenfalls erfasst werden. Alles, was Sie nach Beendigung der Hauptschleife tun müssen, ist, durch die Lüftungsöffnung nach oben zu gleiten. Denken Sie daran, diese zu ersetzen, sobald der Wachmann hereinkommt.

Beachten Sie, dass ich in allen drei dieser realen Anwendungen, die mir tatsächlich passiert sind, wollte, dass der GC so aggressiv wie möglich sammelt. Das ist der Grund, warum dies legitime Anträge sind. Alle anderen liegen falsch.

Jon Harrop
quelle
2
Schwache Referenzen funktionieren in Anwendung 2 nicht, wenn die nicht erreichbaren Untergraphen Zyklen enthalten. Dies liegt daran, dass eine schwache Hash-Tabelle normalerweise schwache Verweise auf die Schlüssel, aber starke Verweise auf die Werte enthält. Sie benötigen eine Hash-Tabelle, die nur dann starke Verweise auf die Werte enthält, wenn der Schlüssel noch erreichbar ist -> siehe Ephemerons ( ConditionalWeakTablein .NET).
Daniel
@Daniel Soll der GC nicht mit nicht erreichbaren Zyklen umgehen können? Wie würde sich dies nicht gesammelt werden , wenn ein nicht erreichbar Zyklus von starken Referenzen würden gesammelt werden?
Binki
Oh, ich glaube ich sehe. Ich habe nur angenommen, dass dies die ConditionalWeakTableAnwendungen 2 und 3 sind, während einige Personen in anderen Posts tatsächlich diese verwenden Dictionary<WeakReference, T>. Keine Ahnung warum - Sie würden immer eine Tonne Nullen WeakReferencemit Werten erhalten, auf die von keinem Schlüssel zugegriffen werden kann, unabhängig davon, wie Sie dies tun. Ridik.
Binki
@binki: "Soll der GC nicht mit nicht erreichbaren Zyklen umgehen können? Wie würde dies nicht erfasst, wenn ein nicht erreichbarer Zyklus starker Referenzen erfasst würde?" Sie haben ein Wörterbuch für eindeutige Objekte, die nicht wiederhergestellt werden können. Wenn eines Ihrer Schlüsselobjekte nicht mehr erreichbar ist, kann es entsorgt werden, aber der entsprechende Wert im Wörterbuch wird nicht einmal als theoretisch nicht erreichbar angesehen, da ein gewöhnliches Wörterbuch einen starken Verweis darauf enthält und es am Leben erhält. Sie verwenden also ein schwaches Wörterbuch.
Jon Harrop
@Daniel: "Schwache Referenzen funktionieren in Anwendung 2 nicht, wenn die nicht erreichbaren Untergraphen Zyklen enthalten. Dies liegt daran, dass eine schwache Hash-Tabelle normalerweise schwache Referenzen zu den Schlüsseln, aber starke Referenzen zu den Werten enthält. Dafür benötigen Sie eine Hash-Tabelle behält starke Verweise auf die Werte nur bei, solange der Schlüssel noch erreichbar ist ". Ja. Es ist wahrscheinlich besser, das Diagramm direkt mit Kanten als Zeigern zu codieren, damit der GC es selbst sammelt.
Jon Harrop
19

Angesichts dieser Eigenschaften kann ich mir keine Situation vorstellen, in der schwache Referenzen nützlich wären, vielleicht könnte mich jemand aufklären?

Microsoft-Dokument Schwache Ereignismuster .

In Anwendungen ist es möglich, dass Handler, die an Ereignisquellen angehängt sind, in Abstimmung mit dem Listener-Objekt, das den Handler an die Quelle angehängt hat, nicht zerstört werden. Diese Situation kann zu Speicherlecks führen. Mit Windows Presentation Foundation (WPF) wird ein Entwurfsmuster eingeführt, mit dem dieses Problem behoben werden kann, indem eine dedizierte Manager-Klasse für bestimmte Ereignisse bereitgestellt und eine Schnittstelle für Listener für dieses Ereignis implementiert wird. Dieses Entwurfsmuster ist als schwaches Ereignismuster bekannt.

...

Das Muster für schwache Ereignisse wurde entwickelt, um dieses Speicherverlustproblem zu lösen. Das Muster für schwache Ereignisse kann immer dann verwendet werden, wenn ein Listener sich für ein Ereignis registrieren muss, der Listener jedoch nicht explizit weiß, wann die Registrierung aufgehoben werden muss. Das Muster für schwache Ereignisse kann auch verwendet werden, wenn die Objektlebensdauer der Quelle die nützliche Objektlebensdauer des Listeners überschreitet. (In diesem Fall wird der Nutzen von Ihnen bestimmt.) Das Muster für schwache Ereignisse ermöglicht es dem Listener, sich für das Ereignis zu registrieren und es zu empfangen, ohne die Eigenschaften der Objektlebensdauer des Listeners in irgendeiner Weise zu beeinträchtigen. Tatsächlich bestimmt der implizite Verweis aus der Quelle nicht, ob der Listener zur Garbage Collection berechtigt ist. Die Referenz ist eine schwache Referenz, daher die Benennung des Musters für schwache Ereignisse und der zugehörigen APIs. Der Listener kann unbrauchbar gemacht oder anderweitig zerstört werden, und die Quelle kann fortgesetzt werden, ohne nicht sammelbare Verweise auf einen jetzt zerstörten Gegenstand beizubehalten.

ta.speot.is
quelle
Diese URL wählt automatisch die neueste .NET-Version (derzeit 4.5) aus, in der dieses Thema nicht mehr verfügbar ist. Die Auswahl von .NET 4.0 funktioniert stattdessen ( msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx )
bis zum
13

Lassen Sie mich dies zuerst ausdrücken und darauf zurückkommen:

Eine WeakReference ist nützlich, wenn Sie ein Objekt im Auge behalten möchten, aber NICHT möchten, dass Ihre Beobachtungen verhindern, dass dieses Objekt gesammelt wird

Fangen wir also von vorne an:

Ich entschuldige mich im Voraus für jede unbeabsichtigte Beleidigung, aber ich werde für einen Moment wieder auf "Dick and Jane" -Niveau aufsteigen, da man es seinem Publikum niemals sagen kann.

Wenn Sie also ein Objekt haben X- geben wir es als Instanz von an class Foo-, kann es NICHT alleine leben (meistens wahr). Genauso wie "Kein Mensch ist eine Insel" gibt es nur wenige Möglichkeiten, wie ein Objekt zur Insel heraufgestuft werden kann - obwohl es in der CLR als GC-Root bezeichnet wird. Als GC-Root oder mit einer festgelegten Kette von Verbindungen / Verweisen auf eine GC-Root wird im Grunde genommen bestimmt, ob Foo x = new Foo()Müll gesammelt wird oder nicht .

Wenn Sie nicht durch Haufen- oder Stapelgehen zu einer GC-Wurzel zurückkehren können, sind Sie effektiv verwaist und werden wahrscheinlich im nächsten Zyklus markiert / gesammelt.

Schauen wir uns an dieser Stelle einige schrecklich erfundene Beispiele an:

Erstens unser Foo:

public class Foo 
{
    private static volatile int _ref = 0;
    public event EventHandler FooEvent;
    public Foo()
    {
        _ref++;
        Console.WriteLine("I am #{0}", _ref);
    }
    ~Foo()
    {
        Console.WriteLine("#{0} dying!", _ref--);
    }
}

Ziemlich einfach - es ist nicht threadsicher. Versuchen Sie es also nicht, sondern führen Sie eine grobe "Referenzzählung" der aktiven Instanzen und Dekremente durch, wenn sie abgeschlossen sind.

Schauen wir uns nun Folgendes an FooConsumer:

public class NastySingleton
{
    // Static member status is one way to "get promoted" to a GC root...
    private static NastySingleton _instance = new NastySingleton();
    public static NastySingleton Instance { get { return _instance;} }

    // testing out "Hard references"
    private Dictionary<Foo, int> _counter = new Dictionary<Foo,int>();
    // testing out "Weak references"
    private Dictionary<WeakReference, int> _weakCounter = new Dictionary<WeakReference,int>();

    // Creates a strong link to Foo instance
    public void ListenToThisFoo(Foo foo)
    {
        _counter[foo] = 0;
        foo.FooEvent += (o, e) => _counter[foo]++;
    }

    // Creates a weak link to Foo instance
    public void ListenToThisFooWeakly(Foo foo)
    {
        WeakReference fooRef = new WeakReference(foo);
        _weakCounter[fooRef] = 0;
        foo.FooEvent += (o, e) => _weakCounter[fooRef]++;
    }

    private void HandleEvent(object sender, EventArgs args, Foo originalfoo)
    {
        Console.WriteLine("Derp");
    }
}

Wir haben also ein Objekt, das bereits ein eigenes GC-Stammverzeichnis hat (genauer gesagt, es wird über eine Kette direkt zu der App-Domäne gerootet, in der diese Anwendung ausgeführt wird, aber das ist ein anderes Thema), das über zwei Methoden verfügt Sich an eine FooInstanz zu klinken - lass es uns ausprobieren:

// Our foo
var f = new Foo();

// Create a "hard reference"
NastySingleton.Instance.ListenToThisFoo(f);

// Ok, we're done with this foo
f = null;

// Force collection of all orphaned objects
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Würden Sie nun von oben erwarten, dass das Objekt, auf das einmal Bezug genommen wurde f, "sammelbar" ist?

Nein, denn es gibt ein anderes Objekt, das jetzt einen Verweis darauf enthält - das Dictionaryin dieser Singletonstatischen Instanz.

Ok, lass uns den schwachen Ansatz ausprobieren:

f = new Foo();
NastySingleton.Instance.ListenToThisFooWeakly(f);

// Ok, we're done with this foo
f = null;

// Force collection of all orphaned objects
// This should collect # 2 - you'll see a "#2 dying"
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Wenn wir nun unseren Verweis auf Foodas, was einmal war, verwechseln f, gibt es keine "harten" Verweise mehr auf das Objekt, so dass es sammelbar ist - das WeakReference, was der schwache Zuhörer erschafft, wird das nicht verhindern.

Gute Anwendungsfälle:

  • Ereignishandler (obwohl zuerst gelesen: Schwache Ereignisse in C # )

  • Sie haben eine Situation, in der Sie eine "rekursive Referenz" verursachen würden (dh Objekt A bezieht sich auf Objekt B, das sich auf Objekt A bezieht, auch als "Memory Leak" bezeichnet) (edit: derp, natürlich ist dies nicht der Fall) nicht wahr)

  • Sie möchten etwas an eine Sammlung von Objekten "senden", aber Sie möchten nicht die Sache sein, die sie am Leben erhält. a List<WeakReference>kann leicht gewartet und sogar entfernt werdenref.Target == null

JerKimball
quelle
1
In Bezug auf Ihren zweiten Anwendungsfall verarbeitet der Garbage Collector zirkuläre Verweise einwandfrei. "Objekt A bezieht sich auf Objekt B, das sich auf Objekt A bezieht" ist definitiv kein Speicherverlust.
Joe Daley
@ JoeDaley Ich stimme zu. Der .NET-GC verwendet einen Markierungs- und Sweep-Algorithmus, der (ich glaube, ich erinnere mich daran richtig) alle Objekte zur Sammlung markiert und dann den Referenzen von "Roots" (Referenzen von Objekten auf dem Stapel, statische Objekte) folgt, wobei die Markierung von Objekten zur Sammlung aufgehoben wird . Wenn ein Zirkelverweis vorhanden ist, aber keines der Objekte von einem Stamm aus zugänglich ist, werden die Objekte nicht zur Sammlung freigegeben und sind daher zur Sammlung berechtigt.
ta.speot.is
1
@JoeDaley - Ihr seid natürlich beide richtig - habt es dort gegen Ende gehetzt ... Ich werde das herausarbeiten.
JerKimball
4

Bildbeschreibung hier eingeben

Wie logische Lecks, die wirklich schwer aufzuspüren sind, während Benutzer nur bemerken, dass das Ausführen Ihrer Software über einen längeren Zeitraum immer mehr Speicher beansprucht und immer langsamer wird, bis sie neu gestartet werden? Ich nicht.

Überlegen Sie, was passiert, wenn der Benutzer, wenn er das Entfernen der oben genannten Anwendungsressource anfordert, Thing2ein solches Ereignis unter folgenden Bedingungen nicht ordnungsgemäß behandelt:

  1. Zeiger
  2. Starke Referenzen
  3. Schwache Referenzen

... und unter welchen Umständen ein solcher Fehler wahrscheinlich beim Testen aufgefallen wäre und welcher nicht und wie ein Stealth-Fighter-Käfer unter dem Radar fliegen würde. Geteilte Eigentümerschaft ist häufig eine unsinnige Idee.


quelle
1

Ein sehr anschauliches Beispiel für nützliche schwache Referenzen ist die ConditionalWeakTable , mit der das DLR (unter anderem) zusätzliche "Mitglieder" an Objekte anfügt.

Sie möchten nicht, dass der Tisch das Objekt am Leben erhält. Dieses Konzept könnte ohne schwache Referenzen einfach nicht funktionieren.

Mir scheint jedoch, dass alle Verwendungen für schwache Verweise lange nach dem Hinzufügen zur Sprache kamen, da schwache Verweise seit Version 1.1 Teil von .NET sind. Es scheint nur etwas zu sein, das Sie hinzufügen möchten, damit Sie das Fehlen deterministischer Zerstörung sprachlich nicht in den Hintergrund drängen.

GregRos
quelle
Ich habe tatsächlich festgestellt, dass die Tabelle zwar das Konzept der schwachen Referenzen verwendet, die tatsächliche Implementierung jedoch nicht den WeakReferenceTyp betrifft , da die Situation sehr viel komplexer ist. Es verwendet verschiedene Funktionen, die von der CLR bereitgestellt werden.
GregRos
-2

Wenn Sie die Cache-Ebene mit C # implementiert haben, ist es auch viel besser, Ihre Daten als schwache Referenzen in den Cache zu stellen. Dies könnte dazu beitragen, die Leistung der Cache-Ebene zu verbessern.

Denken Sie, dass dieser Ansatz auch auf die Implementierung von Sitzungen angewendet werden könnte. Da die Sitzung die meiste Zeit ein langlebiges Objekt ist, kann es vorkommen, dass Sie keinen Speicher für neue Benutzer haben. In diesem Fall ist es viel besser, ein anderes Benutzersitzungsobjekt zu löschen, als OutOfMemoryException auszulösen.

Wenn Sie ein großes Objekt in Ihrer Anwendung haben (eine große Nachschlagetabelle usw.), sollte dies sehr selten verwendet werden, und die Neuerstellung eines solchen Objekts ist keine sehr teure Prozedur. Dann ist es eine gute Woche, wenn Sie die Möglichkeit haben, Ihr Gedächtnis freizugeben, wenn Sie das wirklich brauchen.

Ph0en1x
quelle
5
Das Problem mit schwachen Referenzen (siehe die Antwort, auf die ich verwiesen habe) ist, dass sie sehr eifrig gesammelt werden und die Sammlung nicht an die Verfügbarkeit von Speicherplatz gebunden ist. Sie haben also am Ende mehr Cache-Ausfälle, wenn der Arbeitsspeicher nicht unter Druck steht.
1
Für Ihren zweiten Punkt zu großen Objekten heißt es in der MSDN-Dokumentation jedoch, dass lange schwache Referenzen es Ihnen ermöglichen, ein Objekt neu zu erstellen, der Zustand jedoch unvorhersehbar bleibt. Wenn Sie es jedes Mal von Grund auf neu erstellen möchten, warum sollten Sie sich dann die Mühe machen, eine schwache Referenz zu verwenden, wenn Sie nur eine Funktion / Methode aufrufen können, um sie bei Bedarf zu erstellen und eine vorübergehende Instanz zurückzugeben?
Es gibt eine Situation, in der das Zwischenspeichern hilfreich ist: Wenn häufig unveränderliche Objekte erstellt werden, von denen viele identisch sind (z. B. das Lesen vieler Zeilen aus einer Datei, von der viele Duplikate erwartet werden), wird jede Zeichenfolge als neues Objekt erstellt Wenn jedoch eine Zeile mit einer anderen Zeile übereinstimmt, für die bereits ein Verweis vorhanden ist, kann die Speichereffizienz verbessert werden, wenn die neue Instanz verlassen und ein Verweis auf die bereits vorhandene Instanz ersetzt wird. Beachten Sie, dass diese Ersetzung nützlich ist, da die andere Referenz ohnehin beibehalten wird. Wenn es kein Code wäre, sollte der neue beibehalten werden.
Supercat