2013-03-04 3 views
11

ho un test che prevede di passare ma il comportamento del collettore immondizia non è così Supposi:Garbage Collection dovrebbe rimuovere oggetto ma WeakReference.IsAlive ancora restituendo vero

[Test] 
public void WeakReferenceTest2() 
{ 
    var obj = new object(); 
    var wRef = new WeakReference(obj); 

    wRef.IsAlive.Should().BeTrue(); //passes 

    GC.Collect(); 

    wRef.IsAlive.Should().BeTrue(); //passes 

    obj = null; 

    GC.Collect(); 

    wRef.IsAlive.Should().BeFalse(); //fails 
} 

In questo esempio il obj oggetto dovrebbe essere GC e quindi mi aspetterei che la proprietà WeakReference.IsAlive restituisca false.

Sembra che la variabile obj sia stata dichiarata nello stesso ambito di GC.Collect non sia stata raccolta. Se sposto la dichiarazione obj e l'inizializzazione al di fuori del metodo, il test passa.

Qualcuno ha documentazione tecnica di riferimento o spiegazione per questo comportamento?

+1

Hai controllato il codice IL? Inoltre, si comporta allo stesso modo per le versioni di debug e di rilascio? –

+3

La mia ipotesi iniziale è che le ottimizzazioni del compilatore/runtime/processore ti stiano mordendo. Si rendono conto che non stai leggendo mai 'obj', quindi è permesso riordinare le operazioni tra le altre chiamate di metodo. Prova ad aggiungere qualcosa come 'Console.WriteLine (obj == null)' solo per impedire al compilatore di farlo. – Servy

+1

Questo esempio funziona correttamente sulla mia macchina. Sto usando 'Console.WriteLine' per registrare il parametro' IsAlive' invece di 'Should()' – JaredPar

risposta

6

Colpisci lo stesso problema di te: il mio test è stato trasmesso ovunque, ad eccezione di NCrunch (potrebbe essere qualsiasi altra strumentazione nel tuo caso). Hm. Il debugging con SOS ha rivelato ulteriori radici contenute in uno stack di chiamate di un metodo di test. La mia ipotesi è che fossero il risultato della strumentazione del codice che disabilitava le ottimizzazioni del compilatore, incluse quelle che calcolano correttamente la raggiungibilità degli oggetti.

La cura qui è piuttosto semplice - non tenere mai forti riferimenti da un metodo che fa GC e verifica la vitalità. Questo può essere facilmente ottenuto con un metodo di aiuto banale. La modifica che segue ha permesso al tuo caso di test di passare a NCrunch, dove inizialmente era fallito.

[TestMethod] 
public void WeakReferenceTest2() 
{ 
    var wRef2 = CallInItsOwnScope(() => 
    { 
     var obj = new object(); 
     var wRef = new WeakReference(obj); 

     wRef.IsAlive.Should().BeTrue(); //passes 

     GC.Collect(); 

     wRef.IsAlive.Should().BeTrue(); //passes 
     return wRef; 
    }); 

    GC.Collect(); 

    wRef2.IsAlive.Should().BeFalse(); //used to fail, now passes 
} 

private T CallInItsOwnScope<T>(Func<T> getter) 
{ 
    return getter(); 
} 
+0

Grazie per questo! Una soluzione molto elegante. Stavo usando anche NCrunch. – TechnoTone

+0

Questa soluzione ha funzionato per me anche se l'ho semplificata un po '. Ho inserito le operazioni "CallInItsOwnScope" in una funzione separata anziché in una lambda e questo ha permesso al GC forzato di funzionare come previsto. – Kent

2

Potrebbe essere che il metodo di estensione .Should() sia in qualche modo collegato a un riferimento? O forse qualche altro aspetto del framework di test sta causando questo problema.

(sto postando questo come una risposta altrimenti non può facilmente inserire il codice!)

ho provato il seguente codice, e funziona come previsto (Visual Studio 2012, .Net 4 costruzione, debug e rilascio, a 32 bit e 64 bit, in esecuzione su Windows 7, processore quad core):

using System; 

namespace Demo 
{ 
    internal class Program 
    { 
     private static void Main(string[] args) 
     { 
      var obj = new object(); 
      var wRef = new WeakReference(obj); 

      GC.Collect(); 
      obj = null; 
      GC.Collect(); 

      Console.WriteLine(wRef.IsAlive); // Prints false. 
      Console.ReadKey(); 
     } 
    } 
} 

Cosa succede quando si tenta questo codice?

+2

Se 'Should' è in attesa su un riferimento, dovrebbe essere in una variabile statica, altrimenti non sarà più disponibile e verrà raccolto. – Servy

+0

D'accordo, e sembra improbabile. Ma senza vedere il codice sorgente per questo, devo assumere la possibilità. Anche se penso che sia davvero un "comportamento indefinito" rispetto al momento in cui i riferimenti annullati diventano effettivamente obsoleti, come suggerito. –

+0

Il "Should" proviene dalla libreria FluentAssertions. Ricevo lo stesso comportamento usando Assert.False. – TechnoTone

4

Per quanto ne so, chiamando Collect non garanzia che tutte le risorse vengono rilasciate. Stai semplicemente facendo un suggerimento al netturbino.

Si potrebbe provare a forzarlo a bloccare fino a quando tutti gli oggetti vengono rilasciati in questo modo:

GC.Collect(2, GCCollectionMode.Forced, true); 

mi aspetto che questo potrebbe non funzionare assolutamente al 100% del tempo. In generale, eviterei di scrivere qualsiasi codice che dipende dall'osservazione del garbage collector, non è realmente progettato per essere usato in questo modo.

9

Ci sono alcuni problemi potenziali posso vedere:

  • non sono a conoscenza di nulla nel # specifica C, che richiede che le vite di variabili locali siano limitati. In una build non di debug, penso che il compilatore sarebbe libero di omettere l'ultimo incarico a obj (impostandolo su null) poiché nessun percorso di codice causerebbe mai il valore di obj non verrà mai utilizzato dopo di esso, ma mi aspetto che in una build non di debug dei metadati indicherebbe che la variabile non viene mai utilizzata dopo la creazione del riferimento debole. In una build di debug, la variabile deve esistere nell'intero ambito della funzione, ma l'istruzione obj = null; dovrebbe in realtà cancellarla. Nondimeno, non sono sicuro che la specifica C# prometta che il compilatore non tralascierà l'ultima affermazione e tuttavia mantenga la variabile intorno.

  • Se si utilizza un garbage collector concorrente, è possibile che GC.Collect() attivi l'avvio immediato di una raccolta, ma che la raccolta non venga effettivamente completata prima dei resi GC.Collect().In questo scenario, potrebbe non essere necessario attendere l'esecuzione di tutti i finalizzatori e quindi GC.WaitForPendingFinalizers() potrebbe essere eccessivo, ma probabilmente risolverebbe il problema.

  • Quando si utilizza il garbage collector standard, non mi aspetto l'esistenza di un riferimento debole a un oggetto per prolungare l'esistenza dell'oggetto nel modo in cui lo farebbe un finalizzatore, ma quando si utilizza un garbage collector concorrente, è possibile gli oggetti abbandonati a cui esiste un riferimento debole vengono spostati in una coda di oggetti con riferimenti deboli che devono essere ripuliti e che l'elaborazione di tale pulizia avviene su un thread separato che viene eseguito in concomitanza con tutto il resto. In tal caso, sarebbe necessaria una chiamata a GC.WaitForPendingFinalizers() per ottenere il comportamento desiderato.

noti che si dovrebbe generalmente non aspettarsi che i riferimenti deboli decade con qualsiasi particolare grado di tempestività, né deve aspettare che il recupero Target dopo IsAlive rapporti vero produrrà un riferimento non nullo. Si dovrebbe usare IsAlive solo nei casi in cui a qualcuno non interesserebbe il bersaglio se è ancora vivo, ma sarebbe interessato a sapere che il riferimento è morto. Ad esempio, se si ha una collezione di oggetti WeakReference, si potrebbe desiderare di ripetere periodicamente l'elenco e rimuovere gli oggetti WeakReference il cui target è morto. Si dovrebbe essere preparati alla possibilità che WeakReferences rimanga nella collezione più a lungo di quanto sarebbe idealmente necessario; l'unica conseguenza se lo fanno dovrebbe essere un leggero spreco di memoria e tempo CPU.

0

Ho la sensazione che è necessario chiamare GC.WaitForPendingFinalizers() come mi aspetto che settimana riferimenti vengono aggiornati dal thread finalizzatori.

Ho avuto problemi con molti anni fa durante la scrittura di un test unitario e ricordo che WaitForPendingFinalizers() ha aiutato, così ha fatto per le chiamate a GC.Collect().

Il software non è mai trapelato nella vita reale, ma scrivere un test unitario per dimostrare che l'oggetto non era tenuto in vita era molto più difficile di quanto speravo. (Abbiamo avuto bug in passato con la nostra cache che lo ha tenuto in vita.)