Quando usar referências fracas no .Net?

56

Pessoalmente, não deparei com uma situação em que precisei usar o tipo WeakReference no .Net, mas a crença popular parece ser que ele deve ser usado em caches. O Dr. Jon Harrop apresentou um argumento muito bom contra o uso de WeakReferences em caches na resposta a esta pergunta.

Também ouvi muitas vezes desenvolvedores do AS3 falarem sobre o uso de referências fracas para economizar espaço na memória, mas com base nas conversas que tive, parece acrescentar complexidade sem necessariamente atingir o objetivo pretendido, e o comportamento do tempo de execução é imprevisível. Tanto é assim que muitos simplesmente desistem e, em vez disso, gerenciam o uso da memória com mais cuidado / otimizam seu código para consumir menos memória (ou fazer o trade-off de mais ciclos de CPU e menor espaço de memória).

O Dr. Jon Harrop também apontou em sua resposta que as referências fracas do .Net não são flexíveis, e há uma coleção agressiva de referências fracas no gen0. De acordo com o MSDN , referências longas e fracas oferecem a possibilidade de recriar um objeto but the state of the object remains unpredictable.!

Dadas essas características, não consigo pensar em uma situação em que referências fracas sejam úteis, talvez alguém possa me esclarecer?

theburningmonk
fonte
3
Você já descreveu possíveis usos para ele. É claro que existem outras maneiras de abordar essas situações, mas há mais de uma maneira de esfolar um gato. Se você está procurando uma prova de bala "você deve sempre usar uma WeakReference quando X", duvido que você encontre uma.
2
@ itsme86 - Não estou procurando um caso de uso à prova de balas, apenas aqueles para as quais referências fracas são adequadas e fazem sentido. O caso de uso de cache, por exemplo, porque as referências fracas são tão ansiosamente recolhidos só vai causar mais erros de cache, mesmo quando você tem muita memória disponível
4
Estou um pouco decepcionado com o fato de alguns votos serem encerrados. Eu não me importaria de ver uma resposta ou alguma discussão sobre isso (na b4 "Stack Overflow não é um fórum").
ta.speot.is
@theburningmonk Esse é o deslocamento em troca do ganho de memória. Na estrutura atual, é duvidoso que alguém atinja diretamente a ferramenta WeakReference, mesmo ao implementar um cache, uma vez que existem sistemas abrangentes de cache prontamente disponíveis.
Aqui está um exemplo embaraçosamente sobre-complicada de usá-los (para o padrão de evento fraca que ta.speot.is descreve abaixo)
Benjol

Respostas:

39

Encontrei aplicativos práticos legítimos de referências fracas nos três cenários a seguir que realmente aconteceram comigo pessoalmente:

Aplicativo 1: Manipuladores de eventos

Você é um empreendedor. Sua empresa vende um controle de linhas de ignição para o WPF. As vendas são excelentes, mas os custos de suporte estão prejudicando você. Muitos clientes reclamam de problemas de CPU e vazamentos de memória quando percorrem telas cheias de linhas de centelha. O problema é que o aplicativo está criando novas linhas de ignição à medida que são exibidas, mas a ligação de dados está impedindo que as antigas sejam coletadas de lixo. O que você faz?

Introduzir uma referência fraca entre a ligação de dados e seu controle, para que somente a ligação de dados não impeça mais a coleta de lixo do seu controle. Em seguida, adicione um finalizador ao seu controle que derruba a ligação de dados quando ela é coletada.

Aplicativo 2: Gráficos mutáveis

Você é o próximo John Carmack. Você inventou uma nova e engenhosa representação gráfica de superfícies de subdivisão hierárquica que faz com que os jogos de Tim Sweeney pareçam um Nintendo Wii. Obviamente, não vou lhe dizer exatamente como ele funciona, mas tudo se concentra nesse gráfico mutável, onde os vizinhos de um vértice podem ser encontrados em a Dictionary<Vertex, SortedSet<Vertex>>. A topologia do gráfico continua mudando à medida que o player corre. Há apenas um problema: sua estrutura de dados está descartando subgráficos inacessíveis à medida que é executada e você precisa removê-los ou perderá memória. Felizmente, você é um gênio e sabe que existe uma classe de algoritmos projetados especificamente para localizar e coletar subgráficos inacessíveis: coletores de lixo! Você leu a excelente monografia de Richard Jones sobre o assuntomas deixa você perplexo e preocupado com seu prazo iminente. O que você faz?

Simplesmente, substituindo o seu Dictionarypor uma tabela de hash fraca, você pode pegar o GC existente e coletar automaticamente seus subgráficos inacessíveis para você! Voltar a folhear anúncios da Ferrari.

Aplicação 3: Decoração de árvores

Você está pendurado no teto de uma sala cíclica em um teclado. Você tem 60 segundos para examinar alguns GRANDES DADOS antes que alguém o encontre. Você veio preparado com um belo analisador baseado em fluxo que depende do GC para coletar fragmentos de AST depois de analisados. Mas você percebe que precisa de metadados extras em cada AST Nodee precisa rapidamente. O que você faz?

Você pode usar a Dictionary<Node, Metadata>para associar metadados a cada nó, mas, a menos que você os limpe, as referências fortes do dicionário para os nós AST antigos os manterão vivos e vazarão memória. A solução é uma tabela de hash fraca que mantém apenas referências fracas a chaves e o lixo coleta associações de valor-chave quando a chave se torna inacessível. Em seguida, à medida que os nós AST se tornam inacessíveis, eles são coletados como lixo e sua ligação de valor-chave é removida do dicionário, deixando os metadados correspondentes inacessíveis, para que também sejam coletados. Então, tudo o que você precisa fazer depois que o loop principal terminar é deslizar de volta pelo respiradouro, lembrando-se de substituí-lo assim que o guarda de segurança entrar.

Observe que nas três aplicações do mundo real que realmente aconteceram comigo, eu queria que o GC coletasse o mais agressivamente possível. É por isso que esses são aplicativos legítimos. Todo mundo está errado.

Jon Harrop
fonte
2
Referências fracas não funcionarão para o aplicativo 2 se os subgráficos inacessíveis contiverem ciclos. Isso ocorre porque uma tabela de hash fraca geralmente tem referências fracas às chaves, mas referências fortes aos valores. Você precisaria de uma tabela de hash que mantenha fortes referências aos valores apenas enquanto a chave ainda estiver acessível -> veja efêmeros ( ConditionalWeakTableem .NET).
Daniel
@ Daniel Daniel O GC não deveria ser capaz de lidar com ciclos inacessíveis? Como isso não seria coletado quando um ciclo inacessível de referências fortes seria coletado?
binki
Oh, acho que estou vendo. Eu apenas assumi que ConditionalWeakTableé isso que os aplicativos 2 e 3 usariam, enquanto algumas pessoas em outros posts realmente usam Dictionary<WeakReference, T>. Não sei por quê - você sempre terminaria com uma tonelada de nulos WeakReferencecom valores que não podem ser acessados ​​por nenhuma chave, independentemente de como você o faz. Ridik.
binki
@binki: "O GC não deveria ser capaz de lidar com ciclos inacessíveis? Como isso não seria coletado quando um ciclo inacessível de referências fortes seria coletado?". Você tem um dicionário digitado em objetos exclusivos que não podem ser recriados. Quando um de seus principais objetos se torna inacessível, ele pode ser coletado como lixo, mas o valor correspondente no dicionário nem será considerado teoricamente inacessível porque um dicionário comum mantém uma forte referência a ele, mantendo-o vivo. Então você usa um dicionário fraco.
Jon Harrop 27/02
@ Daniel: "Referências fracas não funcionarão para o aplicativo 2 se os subgráficos inacessíveis contiverem ciclos. Isso ocorre porque uma tabela hash fraca geralmente possui referências fracas às chaves, mas referências fortes aos valores. Você precisaria de uma tabela hash que mantém fortes referências aos valores apenas enquanto a chave ainda está acessível ". Sim. Provavelmente, é melhor codificar o gráfico diretamente com arestas como ponteiros, para que o GC o colete ele mesmo.
Jon Harrop 27/02
19

Dadas essas características, não consigo pensar em uma situação em que referências fracas sejam úteis, talvez alguém possa me esclarecer?

Documento da Microsoft Weak Event Patterns .

Nos aplicativos, é possível que os manipuladores anexados às fontes de eventos não sejam destruídos em coordenação com o objeto de ouvinte que anexou o manipulador à origem. Essa situação pode levar a vazamentos de memória. O Windows Presentation Foundation (WPF) apresenta um padrão de design que pode ser usado para solucionar esse problema, fornecendo uma classe de gerente dedicada para eventos específicos e implementando uma interface nos ouvintes para esse evento. Esse padrão de design é conhecido como padrão de evento fraco.

...

O padrão de evento fraco foi projetado para resolver esse problema de vazamento de memória. O padrão de evento fraco pode ser usado sempre que um ouvinte precisar se registrar para um evento, mas o ouvinte não sabe explicitamente quando cancelar o registro. O padrão de evento fraco também pode ser usado sempre que a vida útil do objeto da origem exceder a vida útil útil do objeto do ouvinte. (Nesse caso, útil é determinado por você.) O padrão de evento fraco permite que o ouvinte se registre e receba o evento sem afetar as características de duração do objeto do ouvinte. Com efeito, a referência implícita da fonte não determina se o ouvinte é elegível para coleta de lixo. A referência é uma referência fraca, portanto, a nomeação do padrão de evento fraco e as APIs relacionadas. O ouvinte pode ser coletado como lixo ou destruído de outra forma, e a fonte pode continuar sem reter referências de manipulador não coletáveis ​​a um objeto agora destruído.

ta.speot.is
fonte
Esse URL seleciona automaticamente a versão mais recente do .NET (4.5 atualmente) na qual 'este tópico não está mais disponível'. A seleção do .NET 4.0 funciona ( msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx )
maxp
13

Deixe-me colocar isso em primeiro lugar e voltar a ele:

Um WeakReference é útil quando você deseja manter abas em um objeto, mas NÃO deseja que suas observações impeçam a coleta desse objeto.

Então, vamos começar do começo:

- desculpe-me antecipadamente por qualquer ofensa não intencional, mas voltarei ao nível "Dick and Jane" por um momento, já que nunca se pode contar para o público.

Então, quando você tem um objeto X- vamos especificá-lo como uma instância class Foodele - ele NÃO PODE viver sozinho (principalmente verdadeiro); Da mesma maneira que "Nenhum homem é uma ilha", existem apenas algumas maneiras pelas quais um objeto pode ser promovido para a Ilha da Ilha - embora seja chamado de raiz da GC no CLR. Ser uma raiz do GC ou ter uma cadeia estabelecida de conexões / referências a uma raiz do GC é basicamente o que determina se o Foo x = new Foo()lixo é coletado ou não .

Se você não conseguir voltar para alguma raiz do GC por meio de heap ou stack stack, ficará efetivamente órfão e provavelmente será marcado / coletado no próximo ciclo.

Neste ponto, vejamos alguns exemplos horríveis:

Primeiro, nosso Foo:

public class Foo 
{
    private static volatile int _ref = 0;
    public event EventHandler FooEvent;
    public Foo()
    {
        _ref++;
        Console.WriteLine("I am #{0}", _ref);
    }
    ~Foo()
    {
        Console.WriteLine("#{0} dying!", _ref--);
    }
}

Bastante simples - não é seguro para threads, por isso não tente fazer isso, mas mantém uma "contagem de referência" aproximada de instâncias ativas e diminui quando elas são finalizadas.

Agora vamos dar uma olhada em FooConsumer:

public class NastySingleton
{
    // Static member status is one way to "get promoted" to a GC root...
    private static NastySingleton _instance = new NastySingleton();
    public static NastySingleton Instance { get { return _instance;} }

    // testing out "Hard references"
    private Dictionary<Foo, int> _counter = new Dictionary<Foo,int>();
    // testing out "Weak references"
    private Dictionary<WeakReference, int> _weakCounter = new Dictionary<WeakReference,int>();

    // Creates a strong link to Foo instance
    public void ListenToThisFoo(Foo foo)
    {
        _counter[foo] = 0;
        foo.FooEvent += (o, e) => _counter[foo]++;
    }

    // Creates a weak link to Foo instance
    public void ListenToThisFooWeakly(Foo foo)
    {
        WeakReference fooRef = new WeakReference(foo);
        _weakCounter[fooRef] = 0;
        foo.FooEvent += (o, e) => _weakCounter[fooRef]++;
    }

    private void HandleEvent(object sender, EventArgs args, Foo originalfoo)
    {
        Console.WriteLine("Derp");
    }
}

Portanto, temos um objeto que já é uma raiz própria do GC (bem ... para ser específico, ele será enraizado através de uma cadeia diretamente no domínio do aplicativo que está executando esse aplicativo, mas esse é outro tópico) que possui dois métodos de travar em uma Fooinstância - vamos testá-lo:

// Our foo
var f = new Foo();

// Create a "hard reference"
NastySingleton.Instance.ListenToThisFoo(f);

// Ok, we're done with this foo
f = null;

// Force collection of all orphaned objects
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Agora, pelo exposto, você esperaria que o objeto que antes era referido ffosse "colecionável"?

Não, porque agora existe outro objeto com uma referência a ele - o Dictionarynessa Singletoninstância estática.

Ok, vamos tentar a abordagem fraca:

f = new Foo();
NastySingleton.Instance.ListenToThisFooWeakly(f);

// Ok, we're done with this foo
f = null;

// Force collection of all orphaned objects
// This should collect # 2 - you'll see a "#2 dying"
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Agora, quando analisamos nossa referência ao Fooque era uma vez f, não há mais referências "difíceis" ao objeto, portanto ele é colecionável - o WeakReferencecriado pelo ouvinte fraco não impede isso.

Bons casos de uso:

  • Manipuladores de eventos (embora leia isto primeiro: Eventos Fracos em C # )

  • Você tem uma situação em que poderia causar uma "referência recursiva" (ou seja, o objeto A se refere ao objeto B, que se refere ao objeto A, também conhecido como "vazamento de memória") (edit: derp, é claro que isso não é é verdade)

  • Você deseja "transmitir" algo para uma coleção de objetos, mas não quer ser o que os mantém vivos; um List<WeakReference>pode ser mantido facilmente e até podado removendo onderef.Target == null

JerKimball
fonte
11
Em relação ao seu segundo caso de uso, o coletor de lixo lida com referências circulares muito bem. "objeto A se refere ao objeto B, que se refere ao objeto A" definitivamente não é um vazamento de memória.
Joe Daley
@JoeDaley Eu concordo. O .NET GC usa um algoritmo de marca e varredura que (acredito que estou lembrando isso corretamente) marca todos os objetos para coleção e segue as referências de "raízes" (referências de objetos na pilha, objetos estáticos), desmarcando objetos para coleção . Se existir uma referência circular, mas nenhum dos objetos estiver acessível a partir de uma raiz, os objetos não serão desmarcados para coleta e, portanto, serão elegíveis para coleta.
ta.speot.is
11
@ JoeDaley - Vocês dois, é claro, estão corretos - estavam correndo lá no final ... Vou editar isso.
precisa saber é o seguinte
4

insira a descrição da imagem aqui

Como vazamentos lógicos que são realmente difíceis de rastrear, enquanto os usuários tendem a notar que a execução do software por um longo tempo tende a ocupar cada vez mais memória e a ficar cada vez mais lenta até serem reiniciados? Eu não.

Considere o que acontece se, quando o usuário solicitar a remoção do recurso do aplicativo acima, Thing2falhar ao lidar adequadamente com esse evento em:

  1. Ponteiros
  2. Referências Fortes
  3. Referências fracas

... e sob o qual um desses erros provavelmente seria detectado durante os testes e qual não seria e voaria sob o radar como um bug de caça furtivo. Propriedade compartilhada é, com mais frequência do que a maioria, uma ideia sem sentido.


fonte
1

Um exemplo muito ilustrativo de referências fracas usadas com bom efeito é o ConditionalWeakTable , usado pelo DLR (entre outros locais) para anexar "membros" adicionais aos objetos.

Você não quer que a mesa mantenha o objeto vivo. Este conceito simplesmente não poderia funcionar sem referências fracas.

Mas parece-me que todos os usos para referências fracas vieram muito tempo depois de serem adicionados à linguagem, pois as referências fracas fazem parte do .NET desde a versão 1.1. Parece apenas algo que você gostaria de adicionar, de modo que a falta de destruição determinística não o levará a um canto no que diz respeito aos recursos da linguagem.

GregRos
fonte
Na verdade, descobri que, embora a tabela use o conceito de referências fracas, a implementação real não envolve o WeakReferencetipo, pois a situação é muito mais complexa. Ele usa diferentes funcionalidades expostas pelo CLR.
GregRos 11/01
-2

Se você possui uma camada de cache implementada com C #, é muito melhor colocar seus dados no cache como referências fracas, isso pode ajudar a melhorar o desempenho da camada de cache.

Pense que essa abordagem também pode ser aplicada à implementação da sessão. Como a sessão é um objeto de vida longa na maioria das vezes, pode ser que você não tenha memória para um novo usuário. Nesse caso, será muito melhor excluir outro objeto de sessão do usuário do que lançar OutOfMemoryException.

Além disso, se você tiver um objeto grande no seu aplicativo (alguma tabela grande de pesquisa, etc.), isso deve ser usado raramente e a recriação de um objeto desse tipo não é um procedimento muito caro. Então, é melhor ter como referência uma semana uma maneira de liberar sua memória quando você realmente precisar disso.

Ph0en1x
fonte
5
mas o problema com referências fracas (veja a resposta que eu referenciei) é que elas são coletadas com muita ansiedade e a coleção não está vinculada à disponibilidade de espaço na memória. Então você acaba com mais falhas de cache quando não há pressão na memória.
11
Mas, para o seu segundo ponto sobre objetos grandes, o documento do MSDN afirma que, embora longas referências fracas permitam a você recriar um objeto, seu estado permanece imprevisível. Se você deseja recriá-lo do zero a cada vez, por que se preocupar em usar uma referência fraca quando você pode simplesmente chamar uma função / método para criá-la sob demanda e retornar uma instância transitória?
Há uma situação em que o armazenamento em cache é útil: se alguém criar objetos imutáveis ​​com frequência, muitos dos quais serão idênticos (por exemplo, ler muitas linhas de um arquivo que deve ter muitas duplicatas) cada sequência será criada como um novo objeto , mas se uma linha corresponder a outra linha à qual já existe uma referência, a eficiência da memória poderá ser melhorada se a nova instância for abandonada e uma referência à instância preexistente for substituída. Observe que essa substituição é útil porque a outra referência está sendo mantida de qualquer maneira. Se não fosse o código, deveria manter o novo.
Supercat 27/02