WeakReference si comporta in modo diverso tra .Net Framework e .Net Core
Considera il codice seguente:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
#nullable enable
namespace ConsoleApp1
{
class Program
{
static void Main()
{
var list = makeList();
var weakRef = new WeakReference(list[0]);
list[0] = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(weakRef.IsAlive);
}
[MethodImpl(MethodImplOptions.NoInlining)]
static List<int[]?> makeList()
{
return new List<int[]?> { new int[2] };
}
}
}
- Con una versione o una build di debug su .Net Framework 4.8, quel codice viene stampato
False
. - Con una versione o una build di debug su .Net Core 3.1, quel codice viene stampato
True
.
Qual è la causa di questa differenza di comportamento? (Sta causando il fallimento di alcuni dei nostri unit test.)
Nota: ho inserito l'inizializzazione dell'elenco makeList()
e disattivato l'inlining nel tentativo di far funzionare la versione .Net Core come la versione .Net Framework, ma senza alcun risultato.
[EDIT] Come ha sottolineato Hans, l'aggiunta di un ciclo lo risolve.
Verrà stampato il codice seguente False
:
var list = makeList();
var weakRef = new WeakReference(list[0]);
list[0] = null;
for (int i = 0; i < 1; ++i)
GC.Collect();
Console.WriteLine(weakRef.IsAlive);
Ma questo stamperà True
:
var list = makeList();
var weakRef = new WeakReference(list[0]);
list[0] = null;
GC.Collect();
GC.Collect();
GC.Collect();
GC.Collect();
// Doesn't seem to matter how many GC.Collect() calls you do.
Console.WriteLine(weakRef.IsAlive);
Questo ha ottenuto di essere una sorta di strana cosa Jitter ...
Risposte
Solo perché è permesso raccogliere qualcosa non significa che sia obbligatorio ritirarlo il prima possibile. Mentre la lingua afferma che il GC è autorizzato a determinare che una variabile locale non viene mai più letta, e quindi non considerarla una radice, ciò non significa che puoi fare affidamento sul contenuto di una variabile locale raccolto immediatamente dopo l'ultima lettura da essa .
Non si tratta di una modifica tra il comportamento definito nel runtime, si tratta di un comportamento indefinito in entrambi i runtime, quindi le differenze tra loro sono del tutto accettabili.
Ho ottenuto il riferimento da liberare, quando ho rimosso la variabile di elenco:
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace NUnitTestProject1
{
public class Tests
{
[TestCase(2, GCCollectionMode.Forced, true)]
public void TestWeakReferenceWithList(int generation, GCCollectionMode forced, bool blocking)
{
static WeakReference CreateWeakReference()
{
return new WeakReference(new List<int[]> { new int[2] });
}
var x = CreateWeakReference();
Assert.IsTrue(x.IsAlive);
GC.Collect(generation, forced, blocking);
Assert.IsFalse(x.IsAlive);
}
}
}
Il seguente test case ha esito negativo:
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace NUnitTestProject1
{
public class Tests
{
[TestCase(2, GCCollectionMode.Forced, true)]
public void TestWeakReferenceWithList(int generation, GCCollectionMode forced, bool blocking)
{
static List<int[]> CreateList()
{
return new List<int[]> { new int[2] };
}
WeakReference x;
{
var list = CreateList();
x = new WeakReference(list);
list = null;
}
Assert.IsTrue(x.IsAlive);
GC.Collect(generation, forced, blocking);
Assert.IsFalse(x.IsAlive);
}
}
}
Se guardiamo l'IL possiamo vedere che null è assegnato alla variabile locale 1:
IL_0003: call class [System.Collections]System.Collections.Generic.List`1<int32[]> NUnitTestProject1.Tests::'<TestWeakReferenceWithList>g__CreateList|0_0'()
IL_0008: stloc.1
IL_0009: ldloc.1
IL_000a: newobj instance void [System.Runtime]System.WeakReference::.ctor(object)
IL_000f: stloc.0
IL_0010: ldnull
IL_0011: stloc.1
IL_0012: nop