Quando utilizzare riferimenti deboli in .Net?


56

Personalmente non mi sono imbattuto in una situazione in cui avevo bisogno di usare il tipo WeakReference in .Net, ma la credenza popolare sembra essere che dovrebbe essere usato nelle cache. Nella sua risposta a questa domanda, il dott. Jon Harrop ha fornito un'ottima tesi contro l'uso di riferimenti deboli nelle cache .

Ho anche spesso sentito gli sviluppatori di AS3 parlare dell'utilizzo di riferimenti deboli per risparmiare sull'impronta della memoria, ma sulla base delle conversazioni che ho avuto sembra aggiungere complessità senza necessariamente raggiungere l'obiettivo previsto e il comportamento in fase di esecuzione è piuttosto imprevedibile. Tant'è che molti semplicemente rinunciano e gestiscono invece con maggiore attenzione l'utilizzo della memoria / ottimizzano il loro codice per ridurre la memoria (o fare il trade-off di più cicli di CPU e minore ingombro di memoria).

Il dott. Jon Harrop ha anche sottolineato nella sua risposta che i riferimenti deboli .Net non sono morbidi, e c'è una raccolta aggressiva di riferimenti deboli a gen0. Secondo MSDN , i riferimenti lunghi e deboli offrono il potenziale per ricreare un oggetto but the state of the object remains unpredictable.!

Date queste caratteristiche, non riesco a pensare a una situazione in cui riferimenti deboli sarebbero utili, forse qualcuno potrebbe illuminarmi?


3
Hai già delineato potenziali usi per questo. Naturalmente ci sono altri modi per affrontare queste situazioni, ma c'è più di un modo per scuoiare un gatto. Se stai cercando un proiettile "dovresti sempre usare un riferimento debole quando X", dubito che ne troverai uno.

2
@ itsme86 - Non sto cercando un caso d'uso a prova di proiettile, solo quelli per cui i riferimenti deboli sono adatti e hanno senso. Il caso d'uso della cache, ad esempio, poiché i riferimenti deboli vengono raccolti con tanta entusiasmo, questo causerà ulteriori

4
Sono un po 'deluso dal fatto che si stiano chiudendo alcuni voti. Non mi dispiacerebbe vedere una risposta o qualche discussione al riguardo (nel b4 "StackTranslate.it non è un forum").
ta.speot.è il

@theburningmonk Questo è l'offset in cambio del guadagno di memoria. Nel quadro odierno, è dubbio che chiunque possa raggiungere direttamente lo strumento WeakReference anche quando si implementa una cache poiché sono disponibili sistemi di cache completi prontamente disponibili.

Ecco un esempio imbarazzantemente complicato di usarli (per il modello di eventi deboli che ta.speot.is descrive di seguito)
Benjol

Risposte:


39

Ho trovato legittime applicazioni pratiche di riferimenti deboli nei seguenti tre scenari del mondo reale che mi sono realmente accaduti personalmente:

Applicazione 1: gestori di eventi

Sei un imprenditore. La tua azienda vende un controllo delle linee di scintilla per WPF. Le vendite sono ottime, ma i costi di supporto ti stanno uccidendo. Troppi clienti si lamentano del hogging della CPU e delle perdite di memoria quando scorrono schermate piene di scintille. Il problema è che la loro app sta creando nuove linee di scintilla quando vengono visualizzate, ma l'associazione dei dati sta impedendo che quelle vecchie vengano raccolte. cosa fai?

Introdurre un riferimento debole tra l'associazione di dati e il controllo in modo che l'associazione di dati da sola non impedirà più che il controllo venga raccolto. Quindi aggiungi un finalizzatore al controllo che abbassa l'associazione dei dati quando vengono raccolti.

Applicazione 2: grafici mutabili

Sei il prossimo John Carmack. Hai inventato una nuova geniale rappresentazione grafica delle superfici di suddivisione gerarchica che fa sembrare i giochi di Tim Sweeney come un Nintendo Wii. Ovviamente non ho intenzione di dirti esattamente come funziona, ma è tutto incentrato su questo grafico mutevole in cui si trovano i vicini di un vertice in a Dictionary<Vertex, SortedSet<Vertex>>. La topologia del grafico continua a cambiare mentre il giocatore corre. C'è solo un problema: la tua struttura dati sta perdendo sottografi non raggiungibili mentre è in esecuzione e devi rimuoverli o perderai memoria. Fortunatamente sei un genio, quindi sai che esiste una classe di algoritmi appositamente progettata per individuare e raccogliere sottografi non raggiungibili: i garbage collector! Hai letto l'eccellente monografia di Richard Jones sull'argomentoma ti lascia perplesso e preoccupato per la tua imminente scadenza. cosa fai?

Semplicemente sostituendo il tuo Dictionarycon una tabella di hash debole, puoi trasferire sulle spalle il GC esistente e farlo raccogliere automaticamente i tuoi sottografi irraggiungibili per te! Torna a sfogliare le pubblicità Ferrari.

Applicazione 3: decorazione di alberi

Stai appeso al soffitto di una stanza cilindrica alla tastiera. Hai 60 secondi per setacciare alcuni GRANDI DATI prima che qualcuno ti trovi. Sei stato preparato con un bellissimo parser basato su stream che si basa sul GC per raccogliere frammenti di AST dopo che sono stati analizzati. Ma ti rendi conto che hai bisogno di metadati extra su ogni AST Nodee ne hai bisogno in fretta. cosa fai?

È possibile utilizzare a Dictionary<Node, Metadata>per associare i metadati a ciascun nodo ma, a meno che non vengano cancellati, i riferimenti forti dal dizionario ai vecchi nodi AST li manterranno in vita e perderanno memoria. La soluzione è una tabella hash debole che mantiene solo i riferimenti deboli alle chiavi e garbage raccoglie i binding di valori-chiave quando la chiave diventa irraggiungibile. Quindi, quando i nodi AST diventano irraggiungibili, vengono raccolti in modo inutile e il loro legame chiave-valore viene rimosso dal dizionario lasciando irraggiungibili i corrispondenti metadati in modo che anch'essi vengano raccolti. Quindi tutto quello che devi fare dopo che il tuo loop principale è terminato è far scorrere indietro attraverso la presa d'aria ricordando di sostituirlo appena entra la guardia di sicurezza.

Si noti che in tutte e tre queste applicazioni del mondo reale che mi sono realmente successe volevo che il GC raccogliesse nel modo più aggressivo possibile. Ecco perché si tratta di applicazioni legittime. Tutti gli altri hanno torto.


2
I riferimenti deboli non funzioneranno per l'applicazione 2 se i sottografi non raggiungibili contengono cicli. Questo perché una tabella hash debole di solito ha riferimenti deboli alle chiavi, ma riferimenti forti ai valori. Avresti bisogno di una tabella hash che mantenga forti riferimenti ai valori solo mentre la chiave è ancora raggiungibile -> vedi effemeridi ( ConditionalWeakTablein .NET).
Daniel,

@Daniel Il GC non dovrebbe essere in grado di gestire cicli non raggiungibili? Come questo non essere raccolte quando un ciclo irraggiungibile di riferimenti forti sarebbe essere raccolte?
Helsinki

Oh, penso di vedere. Ho appena pensato che ConditionalWeakTablefosse quello che le applicazioni 2 e 3 avrebbero usato mentre alcune persone in altri post effettivamente usano Dictionary<WeakReference, T>. Non ho idea del perché — finiresti sempre con una tonnellata di null WeakReferencecon valori a cui non è possibile accedere da nessuna chiave indipendentemente da come lo fai. Ridik.
binki,

@binki: "Il GC non dovrebbe essere in grado di gestire cicli irraggiungibili? Come non verrebbe raccolto quando verrebbe raccolto un ciclo irraggiungibile di riferimenti forti?". Hai un dizionario codificato su oggetti unici che non possono essere ricreati. Quando uno dei tuoi oggetti chiave diventa irraggiungibile, può essere garbage collection ma il valore corrispondente nel dizionario non verrà nemmeno pensato che sia teoricamente irraggiungibile perché un dizionario ordinario terrà un forte riferimento ad esso, mantenendolo in vita. Quindi usi un dizionario debole.
Jon Harrop,

@Daniel: "I riferimenti deboli non funzioneranno per l'applicazione 2 se i sottografi non raggiungibili contengono cicli. Questo perché una tabella hash debole di solito ha riferimenti deboli alle chiavi, ma riferimenti forti ai valori. Avresti bisogno di una tabella hash che mantiene forti riferimenti ai valori solo mentre la chiave è ancora raggiungibile ". Sì. Probabilmente stai meglio codificando il grafico direttamente con i bordi come puntatori, quindi il GC lo raccoglierà da solo.
Jon Harrop,

19

Date queste caratteristiche, non riesco a pensare a una situazione in cui riferimenti deboli sarebbero utili, forse qualcuno potrebbe illuminarmi?

Documento Microsoft Modelli di eventi deboli .

Nelle applicazioni, è possibile che i gestori collegati alle origini eventi non vengano distrutti in coordinamento con l'oggetto listener che ha collegato il gestore alla fonte. Questa situazione può causare perdite di memoria. Windows Presentation Foundation (WPF) introduce un modello di progettazione che può essere utilizzato per risolvere questo problema, fornendo una classe manager dedicata per eventi particolari e implementando un'interfaccia sui listener per quell'evento. Questo modello di progettazione è noto come modello di eventi deboli.

...

Il modello di eventi deboli è progettato per risolvere questo problema di perdita di memoria. Il modello di eventi deboli può essere utilizzato ogni volta che un listener deve registrarsi per un evento, ma il listener non sa esplicitamente quando annullare la registrazione. Il modello di eventi deboli può essere utilizzato anche ogni volta che la durata dell'oggetto della sorgente supera la durata utile dell'oggetto dell'ascoltatore. (In questo caso, l'utilità è determinata da te.) Il modello di eventi deboli consente all'ascoltatore di registrarsi e ricevere l'evento senza influire in alcun modo sulle caratteristiche di durata dell'oggetto dell'ascoltatore. In effetti, il riferimento implicito dalla fonte non determina se il listener è idoneo per la garbage collection. Il riferimento è un riferimento debole, quindi la denominazione del modello di eventi deboli e delle relative API. L'ascoltatore può essere garbage collection o altrimenti distrutto e l'origine può continuare senza conservare i riferimenti del gestore non raccoglibili a un oggetto ora distrutto.


Tale URL seleziona automaticamente la versione .NET più recente (attualmente 4.5) in cui "questo argomento non è più disponibile". La selezione di .NET 4.0 invece funziona ( msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx )
massimo

13

Vorrei prima mettere questo fuori e tornare ad esso:

Un riferimento debole è utile quando si desidera tenere sotto controllo un oggetto, ma NON si desidera che le proprie osservazioni impediscano la raccolta di tale oggetto

Quindi partiamo dall'inizio:

- si scusa in anticipo per qualsiasi offesa involontaria, ma tornerò al livello "Dick and Jane" per un momento poiché non si può mai dire al proprio pubblico.

Quindi quando hai un oggetto X- specificiamolo come istanza di class Foo- NON PU CAN vivere da solo (per lo più vero); Allo stesso modo in cui "Nessun uomo è un'isola", ci sono solo alcuni modi in cui un oggetto può essere promosso a Islandità - sebbene si chiami essere una radice GC nel linguaggio CLR. Essere un GC Root o avere una catena consolidata di connessioni / riferimenti a una radice GC, è fondamentalmente ciò che determina se Foo x = new Foo()raccogliere i rifiuti.

Se non riesci a risalire a qualche radice GC senza camminare in heap o stack, sei effettivamente orfano e verrai probabilmente contrassegnato / raccolto nel ciclo successivo.

A questo punto, diamo un'occhiata ad alcuni esempi orribili inventati:

Innanzitutto, il nostro 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--);
    }
}

Abbastanza semplice: non è thread-safe, quindi non provarlo, ma mantiene un "conteggio di riferimento" approssimativo di istanze e decrementi attivi quando vengono finalizzati.

Ora diamo un'occhiata a 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");
    }
}

Quindi abbiamo un oggetto che è già un suo root GC (beh ... per essere precisi, sarà radicato tramite una catena direttamente al dominio dell'app che esegue questa applicazione, ma questo è un altro argomento) che ha due metodi di aggancio a Fooun'istanza - proviamolo:

// 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();

Ora, da quanto sopra, ti aspetteresti che l'oggetto "a cui una volta veniva riferito f" fosse "collezionabile"?

No, perché ora c'è un altro oggetto che contiene un riferimento ad esso - il Dictionaryin Singletonquell'istanza statica.

Ok, proviamo l'approccio debole:

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();

Ora, quando cancelliamo il nostro riferimento a quello Fooche era una volta f, non ci sono più riferimenti "duri" all'oggetto, quindi è raccoglibile - il WeakReferencecreato dall'ascoltatore debole non lo impedirà.

Buoni casi d'uso:

  • Gestori di eventi (anche se leggi prima: Eventi deboli in C # )

  • Hai una situazione in cui potresti causare un "riferimento ricorsivo" (ovvero, l'oggetto A si riferisce all'oggetto B, che si riferisce all'oggetto A, indicato anche come "Perdita di memoria") (modifica: derp, ovviamente questo non è è vero)

  • Volete "trasmettere" qualcosa a una collezione di oggetti, ma non volete essere la cosa che li tiene in vita; a List<WeakReference>può essere mantenuto facilmente e persino eliminato rimuovendo doveref.Target == null


1
Per quanto riguarda il tuo secondo caso d'uso, il Garbage Collector gestisce bene i riferimenti circolari. "L'oggetto A si riferisce all'oggetto B, che si riferisce all'oggetto A" non è sicuramente una perdita di memoria.
Joe Daley,

@JoeDaley Sono d'accordo. Il GC .NET utilizza un algoritmo mark and sweep che (credo di ricordarlo correttamente) contrassegna tutti gli oggetti per la raccolta e quindi segue i riferimenti da "root" (riferimenti di oggetti nello stack, oggetti statici), deselezionando gli oggetti per la raccolta . Se esiste un riferimento circolare ma nessuno degli oggetti è accessibile da una radice, gli oggetti non sono contrassegnati per la raccolta e quindi sono idonei per la raccolta.
ta.speot.è il

1
@JoeDaley - Naturalmente entrambi avete ragione - lo stava precipitando lì verso la fine ... Lo modificherò.
JerKimball

4

inserisci qui la descrizione dell'immagine

Come perdite logiche che sono davvero difficili da rintracciare mentre gli utenti tendono a notare che l'esecuzione del software per molto tempo tende a prendere sempre più memoria e diventa sempre più lenta fino al riavvio? Io non.

Considera cosa succede se, quando l'utente richiede di rimuovere la risorsa dell'applicazione sopra, Thing2non riesce a gestire correttamente un evento del genere in:

  1. puntatori
  2. Riferimenti forti
  3. Riferimenti deboli

... e in base al quale uno di questi errori verrebbe probabilmente colto durante il test e quale non lo farebbe e volerebbe sotto il radar come un insetto di un caccia stealth. La proprietà condivisa è, più spesso della maggior parte, un'idea senza senso.


1

Un esempio molto illustrativo di riferimenti deboli utilizzati con buoni risultati è la tabella condizionale , che viene utilizzata dal DLR (tra le altre parti) per collegare ulteriori "membri" agli oggetti.

Non vuoi che il tavolo mantenga vivo l'oggetto. Questo concetto semplicemente non potrebbe funzionare senza riferimenti deboli.

Ma mi sembra che tutti gli usi dei riferimenti deboli siano arrivati ​​molto tempo dopo che sono stati aggiunti al linguaggio, poiché i riferimenti deboli fanno parte di .NET dalla versione 1.1. Sembra solo qualcosa che vorresti aggiungere, in modo che la mancanza di distruzione deterministica non ti riporti in un angolo per quanto riguarda le caratteristiche del linguaggio.


In realtà ho scoperto che sebbene la tabella utilizzi il concetto di riferimenti deboli, l'implementazione effettiva non coinvolge il WeakReferencetipo, poiché la situazione è molto più complessa. Utilizza diverse funzionalità esposte dal CLR.
GregRos,

-2

Se hai implementato il livello cache con C # è molto meglio mettere i tuoi dati nella cache come riferimenti deboli, potrebbe aiutare a migliorare le prestazioni del livello cache.

Pensa che anche questo approccio possa essere applicato all'implementazione della sessione. Poiché la sessione è oggetto longevo per la maggior parte del tempo, potrebbe essere un caso in cui non si ha memoria per il nuovo utente. In tal caso sarà molto meglio eliminare qualcun altro oggetto sessione utente quindi lanciare OutOfMemoryException.

Inoltre, se hai un oggetto di grandi dimensioni nella tua applicazione (qualche grande tabella di ricerca, ecc.), Dovrebbe essere usato abbastanza di rado e ricreare un oggetto del genere non è una procedura molto costosa. Quindi è meglio avere un riferimento a una settimana per avere un modo per liberare la memoria quando ne hai davvero bisogno.


5
ma il problema con riferimenti deboli (vedi la risposta a cui ho fatto riferimento) è che sono raccolti con grande entusiasmo e la raccolta non è collegata alla disponibilità di spazio di memoria. Quindi finisci con più errori di cache quando non c'è pressione sulla memoria.

1
Ma per il tuo secondo punto sugli oggetti di grandi dimensioni, il documento MSDN afferma che mentre riferimenti lunghi e deboli ti consentono di ricreare un oggetto, il suo stato rimane imprevedibile. Se hai intenzione di ricrearlo da zero ogni volta, perché preoccuparsi di utilizzare un riferimento debole quando puoi semplicemente chiamare una funzione / metodo per crearlo su richiesta e restituire un'istanza temporanea?

C'è una situazione in cui la memorizzazione nella cache è utile: se si crea frequentemente oggetti immutabili, molti dei quali saranno identici (ad esempio, la lettura di molte righe da un file che dovrebbe avere molti duplicati) ogni stringa verrà creata come un nuovo oggetto , ma se una riga corrisponde a un'altra riga a cui esiste già un riferimento, l'efficienza della memoria può essere migliorata se la nuova istanza viene abbandonata e viene sostituito un riferimento all'istanza preesistente. Si noti che questa sostituzione è utile perché l'altro riferimento viene mantenuto comunque. Se non fosse il codice dovrebbe conservare quello nuovo.
supercat,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.