Les informations que je donne ici ne sont pas nouvelles, je viens de les ajouter par souci d'exhaustivité.
L'idée de ce code est assez simple:
- Les objets ont besoin d'un identifiant unique, qui n'existe pas par défaut. Au lieu de cela, nous devons nous fier à la meilleure chose suivante, qui est
RuntimeHelpers.GetHashCode
de nous obtenir une sorte d'identifiant unique
- Pour vérifier l'unicité, cela implique que nous devons utiliser
object.ReferenceEquals
- Cependant, nous aimerions toujours avoir un identifiant unique, j'ai donc ajouté un
GUID
, qui est par définition unique.
- Parce que je n'aime pas tout verrouiller si je n'ai pas à le faire, je ne l'utilise pas
ConditionalWeakTable
.
Combiné, cela vous donnera le code suivant:
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;
}
}
Pour l'utiliser, créez une instance de UniqueIdMapper
et utilisez les GUID qu'il renvoie pour les objets.
Addenda
Donc, il se passe un peu plus ici; laissez-moi écrire un peu plus ConditionalWeakTable
.
ConditionalWeakTable
fait plusieurs choses. Le plus important est qu'il ne se soucie pas du ramasse-miettes, c'est-à-dire que les objets que vous référencez dans ce tableau seront collectés malgré tout. Si vous recherchez un objet, il fonctionne essentiellement de la même manière que le dictionnaire ci-dessus.
Curieux non? Après tout, lorsqu'un objet est collecté par le GC, il vérifie s'il existe des références à l'objet, et s'il y en a, il les collecte. Donc, s'il y a un objet de ConditionalWeakTable
, pourquoi l'objet référencé sera-t-il alors collecté?
ConditionalWeakTable
utilise une petite astuce, que d'autres structures .NET utilisent également: au lieu de stocker une référence à l'objet, elle stocke en fait un IntPtr. Parce que ce n'est pas une vraie référence, l'objet peut être collecté.
Donc, à ce stade, il y a 2 problèmes à résoudre. Tout d'abord, les objets peuvent être déplacés sur le tas, alors qu'allons-nous utiliser comme IntPtr? Et deuxièmement, comment savons-nous que les objets ont une référence active?
- L'objet peut être épinglé sur le tas et son pointeur réel peut être stocké. Lorsque le GC frappe l'objet à supprimer, il le désépingle et le récupère. Cependant, cela signifierait que nous obtenons une ressource épinglée, ce qui n'est pas une bonne idée si vous avez beaucoup d'objets (en raison de problèmes de fragmentation de la mémoire). Ce n'est probablement pas ainsi que cela fonctionne.
- Lorsque le GC déplace un objet, il rappelle, qui peut alors mettre à jour les références. Cela pourrait être la façon dont il est mis en œuvre à en juger par les appels externes
DependentHandle
- mais je pense que c'est un peu plus sophistiqué.
- Pas le pointeur vers l'objet lui-même, mais un pointeur dans la liste de tous les objets du GC est stocké. IntPtr est soit un index, soit un pointeur dans cette liste. La liste ne change que lorsqu'un objet change de génération, à quel point un simple rappel peut mettre à jour les pointeurs. Si vous vous souvenez du fonctionnement de Mark & Sweep, cela a plus de sens. Il n'y a pas d'épinglage et la suppression est comme avant. Je pense que c'est ainsi que cela fonctionne
DependentHandle
.
Cette dernière solution nécessite que le runtime ne réutilise pas les buckets de liste tant qu'ils ne sont pas explicitement libérés, et elle nécessite également que tous les objets soient récupérés par un appel au runtime.
Si nous supposons qu'ils utilisent cette solution, nous pouvons également résoudre le deuxième problème. L'algorithme Mark & Sweep garde la trace des objets qui ont été collectés; dès qu'il a été collecté, nous savons à ce stade. Une fois que l'objet vérifie si l'objet est là, il appelle «Free», ce qui supprime le pointeur et l'entrée de la liste. L'objet est vraiment parti.
Une chose importante à noter à ce stade est que les choses tournent terriblement mal si elle ConditionalWeakTable
est mise à jour dans plusieurs threads et si elle n'est pas sûre pour les threads. Le résultat serait une fuite de mémoire. C'est pourquoi tous les appels ConditionalWeakTable
effectuent un simple «verrouillage» qui garantit que cela ne se produira pas.
Une autre chose à noter est que le nettoyage des entrées doit avoir lieu de temps en temps. Alors que les objets réels seront nettoyés par le GC, les entrées ne le sont pas. C'est pourquoi ConditionalWeakTable
ne grandit qu'en taille. Une fois qu'il atteint une certaine limite (déterminée par le risque de collision dans le hachage), il déclenche un Resize
, qui vérifie si les objets doivent être nettoyés - s'ils le font, free
est appelé dans le processus GC, supprimant la IntPtr
poignée.
Je crois que c'est aussi pourquoi DependentHandle
n'est pas exposé directement - vous ne voulez pas gâcher les choses et obtenir une fuite de mémoire en conséquence. La meilleure chose suivante pour cela est a WeakReference
(qui stocke également IntPtr
un objet au lieu d'un objet) - mais n'inclut malheureusement pas l'aspect «dépendance».
Il ne vous reste plus qu'à jouer avec les mécanismes, afin que vous puissiez voir la dépendance en action. Assurez-vous de le démarrer plusieurs fois et de regarder les résultats:
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
}