WeakReference se comporta de manera diferente entre .Net Framework y .Net Core
Considere el siguiente código:
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 versión o una versión de depuración en .Net Framework 4.8, ese código se imprime
False
. - Con una versión o una versión de depuración en .Net Core 3.1, ese código se imprime
True
.
¿Qué está causando esta diferencia de comportamiento? (Está provocando que algunas de nuestras pruebas unitarias fallen).
Nota: Puse la inicialización de la lista makeList()
y desactivé la inserción en un intento de hacer que la versión .Net Core funcione igual que la versión .Net Framework, pero fue en vano.
[EDITAR] Como señaló Hans, agregar un bucle lo arregla.
El siguiente código se imprima 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);
Pero esto imprimirá 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);
Esto ha llegado a ser una especie de cosa rara ... Jitter
Respuestas
El hecho de que se permita recolectar algo no significa que esté obligado a recolectarlo lo antes posible. Si bien el lenguaje establece que el GC puede determinar que una variable local nunca se vuelve a leer y, por lo tanto, no la considera una raíz, eso no significa que pueda confiar en que el contenido de una variable local se recopile inmediatamente después de la última lectura. .
Este no es un cambio entre el comportamiento definido en el tiempo de ejecución, es un comportamiento indefinido en ambos tiempos de ejecución, por lo que las diferencias entre ellos son completamente aceptables.
Obtuve la referencia para ser liberado, cuando eliminé la variable de lista:
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);
}
}
}
El siguiente caso de prueba falla:
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 miramos el IL, podemos ver que se asigna nulo a la variable local 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