As informações que dou aqui não são novas, apenas adicionei para completar.
A ideia deste código é bastante simples:
- Os objetos precisam de um ID exclusivo, que não existe por padrão. Em vez disso, temos que confiar na próxima melhor coisa, que é
RuntimeHelpers.GetHashCode
nos fornecer uma espécie de ID exclusivo
- Para verificar a exclusividade, isso implica que precisamos usar
object.ReferenceEquals
- No entanto, ainda gostaríamos de ter um ID exclusivo, então adicionei um
GUID
, que por definição é exclusivo.
- Porque não gosto de trancar tudo se não for preciso, não uso
ConditionalWeakTable
.
Combinado, isso fornecerá o seguinte código:
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;
}
}
Para usá-lo, crie uma instância de UniqueIdMapper
e use os GUIDs que retorna para os objetos.
Termo aditivo
Então, há um pouco mais acontecendo aqui; deixe-me escrever um pouco sobre ConditionalWeakTable
.
ConditionalWeakTable
faz algumas coisas. O mais importante é que não se preocupe com o coletor de lixo, ou seja: os objetos que você referenciar nesta tabela serão coletados de qualquer maneira. Se você pesquisar um objeto, ele basicamente funcionará da mesma forma que o dicionário acima.
Curioso, não? Afinal, quando um objeto está sendo coletado pelo CG, ele verifica se há referências ao objeto e, se houver, as coleta. Portanto, se houver um objeto do ConditionalWeakTable
, por que o objeto referenciado será coletado?
ConditionalWeakTable
usa um pequeno truque, que algumas outras estruturas .NET também usam: em vez de armazenar uma referência ao objeto, ele na verdade armazena um IntPtr. Como essa não é uma referência real, o objeto pode ser coletado.
Portanto, neste ponto, há 2 problemas a serem resolvidos. Primeiro, os objetos podem ser movidos no heap, então o que usaremos como IntPtr? E em segundo lugar, como sabemos que os objetos têm uma referência ativa?
- O objeto pode ser fixado no heap e seu ponteiro real pode ser armazenado. Quando o GC atinge o objeto para remoção, ele o desenrosca e o coleta. No entanto, isso significaria que obteríamos um recurso fixado, o que não é uma boa ideia se você tiver muitos objetos (devido a problemas de fragmentação de memória). Provavelmente não é assim que funciona.
- Quando o GC move um objeto, ele chama de volta, que pode então atualizar as referências. Pode ser assim que está implementado a julgar pelas chamadas externas
DependentHandle
- mas acredito que seja um pouco mais sofisticado.
- Não o ponteiro para o próprio objeto, mas um ponteiro na lista de todos os objetos do GC é armazenado. O IntPtr é um índice ou um ponteiro nesta lista. A lista só muda quando um objeto muda de geração, ponto em que um simples retorno de chamada pode atualizar os ponteiros. Se você se lembra de como funciona o Mark & Sweep, isso faz mais sentido. Não há fixação e a remoção é como antes. Eu acredito que é assim que funciona no
DependentHandle
.
Esta última solução exige que o tempo de execução não reutilize os depósitos de lista até que eles sejam explicitamente liberados e também exige que todos os objetos sejam recuperados por uma chamada para o tempo de execução.
Se presumirmos que eles usam essa solução, também podemos resolver o segundo problema. O algoritmo Mark & Sweep mantém registro de quais objetos foram coletados; assim que for coletado, sabemos neste ponto. Depois que o objeto verifica se o objeto está lá, ele chama 'Livre', que remove o ponteiro e a entrada da lista. O objeto realmente se foi.
Uma coisa importante a se observar neste ponto é que as coisas dão terrivelmente errado se ConditionalWeakTable
for atualizado em vários threads e não for seguro para threads. O resultado seria um vazamento de memória. É por isso que todas as chamadas ConditionalWeakTable
fazem um simples 'bloqueio' que garante que isso não aconteça.
Outra coisa a observar é que a limpeza das entradas deve acontecer de vez em quando. Embora os objetos reais sejam limpos pelo GC, as entradas não são. É por isso que ConditionalWeakTable
apenas cresce em tamanho. Uma vez que atinge um certo limite (determinado pela chance de colisão no hash), ele aciona um Resize
, que verifica se os objetos precisam ser limpos - se o fizerem, free
é chamado no processo de GC, removendo o IntPtr
identificador.
Eu acredito que é também por isso que DependentHandle
não é exposto diretamente - você não quer bagunçar as coisas e ter um vazamento de memória como resultado. A próxima melhor coisa para isso é um WeakReference
(que também armazena um em IntPtr
vez de um objeto) - mas infelizmente não inclui o aspecto de 'dependência'.
O que resta é você brincar com a mecânica, para poder ver a dependência em ação. Certifique-se de iniciá-lo várias vezes e observar os resultados:
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
}