WeakReference se comporte différemment entre .Net Framework et .Net Core
Considérez le code suivant:
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] };
}
}
}
- Avec une version ou une version de débogage sur .Net Framework 4.8, ce code s'imprime
False
. - Avec une version ou une version de débogage sur .Net Core 3.1, ce code s'imprime
True
.
Qu'est-ce qui cause cette différence de comportement? (Cela provoque l'échec de certains de nos tests unitaires.)
Remarque: J'ai mis l'initialisation de la liste dans makeList()
et désactivé l'inlining pour tenter de faire fonctionner la version .Net Core de la même manière que la version .Net Framework, mais en vain.
[EDIT] Comme Hans l'a souligné, l'ajout d'une boucle le corrige.
Le code suivant va imprimer 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);
Mais ceci imprimera 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);
Cela a obtenu d'être une sorte de chose bizarre ... Jitter
Réponses
Ce n'est pas parce que quelque chose est autorisé à être collecté qu'il est obligé d'être collecté dès que possible. Bien que le langage indique que le GC est autorisé à déterminer qu'une variable locale n'est plus jamais lue, et donc à ne pas la considérer comme une racine, cela ne signifie pas que vous pouvez compter sur le contenu d'une variable locale collectée immédiatement après votre dernière lecture. .
Ce n'est pas un changement entre le comportement défini dans le runtime, c'est un comportement non défini dans les deux runtimes, donc les différences entre eux sont tout à fait acceptables.
J'ai obtenu la référence à libérer, lorsque j'ai supprimé la variable de liste:
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);
}
}
}
Le scénario de test suivant échoue:
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);
}
}
}
Si nous regardons l'IL, nous pouvons voir que null est assigné à la variable 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