Die Informationen, die ich hier gebe, sind nicht neu. Der Vollständigkeit halber habe ich sie nur hinzugefügt.
Die Idee dieses Codes ist ganz einfach:
- Objekte benötigen eine eindeutige ID, die standardmäßig nicht vorhanden ist. Stattdessen müssen wir uns auf das nächstbeste verlassen
RuntimeHelpers.GetHashCode
, nämlich eine Art eindeutige ID zu erhalten
- Um die Eindeutigkeit zu überprüfen, müssen wir diese verwenden
object.ReferenceEquals
- Wir möchten jedoch immer noch eine eindeutige ID haben, daher habe ich eine hinzugefügt
GUID
, die per Definition eindeutig ist.
- Weil ich nicht gerne alles sperre, wenn ich nicht muss, benutze ich nicht
ConditionalWeakTable
.
In Kombination erhalten Sie den folgenden Code:
public class UniqueIdMapper
{
private class ObjectEqualityComparer : IEqualityComparer<object>
{
public bool Equals(object x, object y)
{
return object.ReferenceEquals(x, y);
}
public int GetHashCode(object obj)
{
return RuntimeHelpers.GetHashCode(obj);
}
}
private Dictionary<object, Guid> dict = new Dictionary<object, Guid>(new ObjectEqualityComparer());
public Guid GetUniqueId(object o)
{
Guid id;
if (!dict.TryGetValue(o, out id))
{
id = Guid.NewGuid();
dict.Add(o, id);
}
return id;
}
}
Um es zu verwenden, erstellen Sie eine Instanz von UniqueIdMapper
und verwenden Sie die zurückgegebenen GUIDs für die Objekte.
Nachtrag
Hier ist also noch ein bisschen mehr los. Lass mich ein bisschen darüber schreiben ConditionalWeakTable
.
ConditionalWeakTable
macht ein paar Dinge. Das Wichtigste ist, dass der Garbage Collector keine Rolle spielt, dh, die Objekte, auf die Sie in dieser Tabelle verweisen, werden unabhängig davon erfasst. Wenn Sie ein Objekt nachschlagen, funktioniert es im Wesentlichen genauso wie das obige Wörterbuch.
Neugierig nein? Wenn ein Objekt vom GC erfasst wird, prüft es schließlich, ob Verweise auf das Objekt vorhanden sind, und erfasst diese, falls vorhanden. Wenn es also ein Objekt aus dem gibt ConditionalWeakTable
, warum wird das referenzierte Objekt dann gesammelt?
ConditionalWeakTable
verwendet einen kleinen Trick, den auch einige andere .NET-Strukturen verwenden: Anstatt einen Verweis auf das Objekt zu speichern, wird tatsächlich ein IntPtr gespeichert. Da dies keine echte Referenz ist, kann das Objekt gesammelt werden.
An dieser Stelle sind also zwei Probleme zu lösen. Erstens können Objekte auf dem Heap verschoben werden. Was werden wir also als IntPtr verwenden? Und zweitens, woher wissen wir, dass Objekte eine aktive Referenz haben?
- Das Objekt kann auf dem Heap fixiert und sein realer Zeiger gespeichert werden. Wenn der GC das Objekt zum Entfernen trifft, löst er es und sammelt es ein. Dies würde jedoch bedeuten, dass wir eine angeheftete Ressource erhalten. Dies ist keine gute Idee, wenn Sie viele Objekte haben (aufgrund von Problemen mit der Speicherfragmentierung). So funktioniert es wahrscheinlich nicht.
- Wenn der GC ein Objekt verschiebt, ruft er zurück, wodurch die Referenzen aktualisiert werden können. So könnte es umgesetzt werden, gemessen an den externen Anrufen
DependentHandle
- aber ich glaube, es ist etwas ausgefeilter.
- Es wird nicht der Zeiger auf das Objekt selbst gespeichert, sondern ein Zeiger in der Liste aller Objekte aus dem GC. Der IntPtr ist entweder ein Index oder ein Zeiger in dieser Liste. Die Liste ändert sich nur, wenn ein Objekt Generationen ändert. Zu diesem Zeitpunkt kann ein einfacher Rückruf die Zeiger aktualisieren. Wenn Sie sich daran erinnern, wie Mark & Sweep funktioniert, ist dies sinnvoller. Es gibt kein Fixieren und das Entfernen ist wie zuvor. Ich glaube, so funktioniert es
DependentHandle
.
Diese letzte Lösung erfordert, dass die Laufzeit die Listen-Buckets erst wiederverwendet, wenn sie explizit freigegeben wurden, und dass alle Objekte durch einen Aufruf der Laufzeit abgerufen werden müssen.
Wenn wir davon ausgehen, dass sie diese Lösung verwenden, können wir auch das zweite Problem angehen. Der Mark & Sweep-Algorithmus verfolgt, welche Objekte gesammelt wurden. Sobald es gesammelt wurde, wissen wir an dieser Stelle. Sobald das Objekt prüft, ob das Objekt vorhanden ist, ruft es 'Free' auf, wodurch der Zeiger und der Listeneintrag entfernt werden. Das Objekt ist wirklich weg.
Eine wichtige Sache, die an dieser Stelle zu beachten ist, ist, dass Dinge schrecklich schief gehen, wenn sie ConditionalWeakTable
in mehreren Threads aktualisiert werden und wenn sie nicht threadsicher sind. Das Ergebnis wäre ein Speicherverlust. Aus diesem Grund führen alle eingehenden Anrufe ConditionalWeakTable
eine einfache Sperre durch, die sicherstellt, dass dies nicht geschieht.
Eine andere Sache zu beachten ist, dass das Bereinigen von Einträgen von Zeit zu Zeit erfolgen muss. Während die tatsächlichen Objekte vom GC bereinigt werden, sind dies die Einträge nicht. Deshalb ConditionalWeakTable
wächst nur die Größe. Sobald es ein bestimmtes Limit erreicht (bestimmt durch die Kollisionswahrscheinlichkeit im Hash), löst es ein aus Resize
, das prüft, ob Objekte bereinigt werden müssen - wenn dies der Fall free
ist , wird es im GC-Prozess aufgerufen und das IntPtr
Handle entfernt.
Ich glaube, dies ist auch der Grund, warum DependentHandle
nicht direkt belichtet wird - Sie möchten sich nicht mit Dingen anlegen und dadurch ein Speicherleck bekommen. Das nächstbeste dafür ist a WeakReference
(das auch IntPtr
ein Objekt anstelle eines Objekts speichert ) - enthält aber leider nicht den Aspekt 'Abhängigkeit'.
Was bleibt, ist, dass Sie mit der Mechanik herumspielen, damit Sie die Abhängigkeit in Aktion sehen können. Starten Sie es mehrmals und sehen Sie sich die Ergebnisse an:
class DependentObject
{
public class MyKey : IDisposable
{
public MyKey(bool iskey)
{
this.iskey = iskey;
}
private bool disposed = false;
private bool iskey;
public void Dispose()
{
if (!disposed)
{
disposed = true;
Console.WriteLine("Cleanup {0}", iskey);
}
}
~MyKey()
{
Dispose();
}
}
static void Main(string[] args)
{
var dep = new MyKey(true); // also try passing this to cwt.Add
ConditionalWeakTable<MyKey, MyKey> cwt = new ConditionalWeakTable<MyKey, MyKey>();
cwt.Add(new MyKey(true), dep); // try doing this 5 times f.ex.
GC.Collect(GC.MaxGeneration);
GC.WaitForFullGCComplete();
Console.WriteLine("Wait");
Console.ReadLine(); // Put a breakpoint here and inspect cwt to see that the IntPtr is still there
}