Le informazioni che fornisco qui non sono nuove, l'ho solo aggiunto per completezza.
L'idea di questo codice è abbastanza semplice:
- Gli oggetti richiedono un ID univoco, che non è presente per impostazione predefinita. Invece, dobbiamo fare affidamento sulla prossima cosa migliore, che è quella
RuntimeHelpers.GetHashCode
di procurarci una sorta di ID univoco
- Per verificare l'unicità, questo implica che dobbiamo usare
object.ReferenceEquals
- Tuttavia, vorremmo comunque avere un ID univoco, quindi ho aggiunto un
GUID
, che è per definizione unico.
- Perché non mi piace bloccare tutto se non devo, non lo uso
ConditionalWeakTable
.
Combinato, questo ti darà il seguente codice:
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;
}
}
Per usarlo, creare un'istanza di UniqueIdMapper
e utilizzare il GUID restituito per gli oggetti.
appendice
Quindi, sta succedendo un po 'di più qui; fammi scrivere un po 'su ConditionalWeakTable
.
ConditionalWeakTable
fa un paio di cose. La cosa più importante è che non si preoccupi del garbage collector, ovvero: gli oggetti a cui fai riferimento in questa tabella verranno raccolti a prescindere. Se cerchi un oggetto, fondamentalmente funziona come il dizionario sopra.
Curioso no? Dopo tutto, quando un oggetto viene raccolto dal GC, controlla se ci sono riferimenti all'oggetto e, se ci sono, li raccoglie. Quindi, se c'è un oggetto da ConditionalWeakTable
, perché l'oggetto di riferimento verrà raccolto?
ConditionalWeakTable
utilizza un piccolo trucco, utilizzato anche da altre strutture .NET: invece di memorizzare un riferimento all'oggetto, memorizza effettivamente un IntPtr. Poiché non è un vero riferimento, l'oggetto può essere raccolto.
Quindi, a questo punto ci sono 2 problemi da affrontare. In primo luogo, gli oggetti possono essere spostati sull'heap, quindi cosa useremo come IntPtr? E secondo, come sappiamo che gli oggetti hanno un riferimento attivo?
- L'oggetto può essere bloccato sull'heap e il suo vero puntatore può essere memorizzato. Quando il GC colpisce l'oggetto per la rimozione, lo apre e lo raccoglie. Tuttavia, ciò significherebbe che avremo una risorsa bloccata, che non è una buona idea se si hanno molti oggetti (a causa di problemi di frammentazione della memoria). Probabilmente non è così che funziona.
- Quando il GC sposta un oggetto, richiama, che può quindi aggiornare i riferimenti. Questo potrebbe essere il modo in cui viene implementato a giudicare dalle chiamate esterne in
DependentHandle
, ma credo sia leggermente più sofisticato.
- Non il puntatore all'oggetto stesso, ma viene memorizzato un puntatore nell'elenco di tutti gli oggetti dal GC. IntPtr è un indice o un puntatore in questo elenco. L'elenco cambia solo quando un oggetto cambia generazioni, a quel punto un semplice callback può aggiornare i puntatori. Se ricordi come funziona Mark & Sweep, questo ha più senso. Non ci sono blocchi e la rimozione è come prima. Credo che sia così che funziona
DependentHandle
.
Quest'ultima soluzione richiede che il runtime non riutilizzi i bucket dell'elenco fino a quando non vengono liberati esplicitamente e richiede anche che tutti gli oggetti vengano recuperati da una chiamata al runtime.
Se supponiamo che utilizzino questa soluzione, possiamo anche affrontare il secondo problema. L'algoritmo Mark & Sweep tiene traccia di quali oggetti sono stati raccolti; non appena è stato raccolto, lo sappiamo a questo punto. Una volta che l'oggetto controlla se l'oggetto è presente, chiama "Libero", che rimuove il puntatore e la voce dell'elenco. L'oggetto è davvero sparito.
Una cosa importante da notare a questo punto è che le cose vanno orribilmente storte se ConditionalWeakTable
viene aggiornato in più thread e se non è thread-safe. Il risultato sarebbe una perdita di memoria. Questo è il motivo per cui tutte le chiamate in entrata ConditionalWeakTable
fanno un semplice "blocco" che garantisce che ciò non avvenga.
Un'altra cosa da notare è che la pulizia delle voci deve essere eseguita di tanto in tanto. Mentre gli oggetti effettivi verranno puliti dal GC, le voci non lo sono. Questo è il motivo per cui ConditionalWeakTable
cresce solo di dimensioni. Una volta raggiunto un certo limite (determinato dalla possibilità di collisione nell'hash), attiva un Resize
, che controlla se gli oggetti devono essere puliti - se lo fanno, free
viene chiamato nel processo GC, rimuovendo la IntPtr
maniglia.
Credo che questo sia anche il motivo per cui DependentHandle
non è esposto direttamente: non vuoi fare confusione con le cose e ottenere come risultato una perdita di memoria. La prossima cosa migliore è a WeakReference
(che memorizza anche un IntPtr
invece di un oggetto) - ma sfortunatamente non include l'aspetto della "dipendenza".
Ciò che resta da fare è giocare con la meccanica, così da poter vedere la dipendenza in azione. Assicurati di avviarlo più volte e guarda i risultati:
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
}