Informacje, które tu podaję, nie są nowe, dodałem je tylko dla kompletności.
Idea tego kodu jest dość prosta:
- Obiekty wymagają unikalnego identyfikatora, którego domyślnie nie ma. Zamiast tego musimy polegać na następnej najlepszej rzeczy, czyli
RuntimeHelpers.GetHashCode
na uzyskaniu swego rodzaju unikalnego identyfikatora
- Aby sprawdzić wyjątkowość, oznacza to, że musimy użyć
object.ReferenceEquals
- Jednak nadal chcielibyśmy mieć unikalny identyfikator, więc dodałem
GUID
, który z definicji jest unikalny.
- Ponieważ nie lubię blokować wszystkiego, jeśli nie muszę, nie używam
ConditionalWeakTable
.
W połączeniu daje to następujący kod:
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;
}
}
Aby z niego skorzystać, utwórz instancję UniqueIdMapper
i użyj identyfikatorów GUID, które zwraca dla obiektów.
Uzupełnienie
Tak więc dzieje się tu trochę więcej; napiszę trochę o tym ConditionalWeakTable
.
ConditionalWeakTable
robi kilka rzeczy. Najważniejsze jest to, że nie obchodzi go garbage collector, to znaczy: obiekty, do których odwołujesz się w tej tabeli, zostaną zebrane niezależnie. Jeśli szukasz obiektu, działa on w zasadzie tak samo, jak powyższy słownik.
Ciekawy nie? W końcu, gdy obiekt jest zbierany przez GC, sprawdza, czy istnieją odwołania do obiektu, a jeśli tak, zbiera je. Więc jeśli istnieje obiekt z ConditionalWeakTable
, dlaczego w takim przypadku będzie zbierany obiekt , do którego się odwołuje?
ConditionalWeakTable
używa małej sztuczki, której również używają inne struktury .NET: zamiast przechowywać odniesienie do obiektu, w rzeczywistości przechowuje IntPtr. Ponieważ to nie jest prawdziwe odniesienie, obiekt można zebrać.
W tym momencie należy rozwiązać dwa problemy. Po pierwsze, obiekty można przenosić na stercie, więc czego użyjemy jako IntPtr? Po drugie, skąd wiemy, że obiekty mają aktywne odniesienie?
- Obiekt można przypiąć na stercie, a jego rzeczywisty wskaźnik można przechowywać. Kiedy GC uderza w obiekt w celu usunięcia, odpina go i zbiera. Oznaczałoby to jednak, że otrzymujemy przypięty zasób, co nie jest dobrym pomysłem, jeśli masz dużo obiektów (ze względu na problemy z fragmentacją pamięci). Prawdopodobnie tak nie działa.
- Kiedy GC przesuwa obiekt, wywołuje z powrotem, który może następnie zaktualizować odwołania. Może tak jest zaimplementowany, sądząc po połączeniach zewnętrznych
DependentHandle
- ale uważam, że jest nieco bardziej wyrafinowany.
- Nie wskaźnik do samego obiektu, ale wskaźnik na liście wszystkich obiektów z GC jest przechowywany. IntPtr jest indeksem lub wskaźnikiem na tej liście. Lista zmienia się tylko wtedy, gdy obiekt zmienia generacje, w którym to momencie proste wywołanie zwrotne może zaktualizować wskaźniki. Jeśli pamiętasz, jak działa Mark & Sweep, ma to większy sens. Nie ma przypinania, a usuwanie jest takie, jak wcześniej. Myślę, że tak to działa
DependentHandle
.
To ostatnie rozwiązanie wymaga, aby środowisko wykonawcze nie użyło ponownie zasobników listy, dopóki nie zostaną one jawnie zwolnione, a także wymaga, aby wszystkie obiekty zostały pobrane przez wywołanie środowiska wykonawczego.
Jeśli założymy, że korzystają z tego rozwiązania, możemy również rozwiązać drugi problem. Algorytm Mark & Sweep śledzi, które obiekty zostały zebrane; jak tylko zostanie zebrany, wiemy w tym miejscu. Gdy obiekt sprawdzi, czy obiekt tam jest, wywołuje „Free”, co usuwa wskaźnik i wpis na liście. Obiekt naprawdę zniknął.
W tym miejscu należy zauważyć, że rzeczy idą strasznie źle, jeśli ConditionalWeakTable
są aktualizowane w wielu wątkach i nie są bezpieczne dla wątków. Rezultatem byłby wyciek pamięci. Dlatego wszystkie połączenia ConditionalWeakTable
wykonują prostą „blokadę”, która zapewnia, że tak się nie stanie.
Inną rzeczą, na którą należy zwrócić uwagę, jest to, że czyszczenie wpisów musi odbywać się od czasu do czasu. Chociaż rzeczywiste obiekty zostaną wyczyszczone przez GC, wpisy nie. Dlatego ConditionalWeakTable
tylko rośnie. Gdy osiągnie pewien limit (określony przez szansę na kolizję w hashu), wyzwala a Resize
, który sprawdza, czy obiekty muszą zostać wyczyszczone - jeśli tak, free
jest wywoływane w procesie GC, usuwając IntPtr
uchwyt.
Uważam, że jest to również powód, dla którego DependentHandle
nie jest on ujawniany bezpośrednio - nie chcesz zepsuć rzeczy i w rezultacie uzyskać wyciek pamięci. Następną najlepszą rzeczą do tego jest a WeakReference
(który również przechowuje IntPtr
zamiast obiektu) - ale niestety nie obejmuje aspektu „zależności”.
Pozostaje ci bawić się mechaniką, abyś mógł zobaczyć zależność w akcji. Pamiętaj, aby uruchomić go wiele razy i obserwować wyniki:
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
}