弱引用(WeakReference)在 .Net Framework 和 .Net Core 中的表现不同。

6

请考虑以下代码:

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] };
        }
    }
}
  • 在 .Net Framework 4.8 的 release 或 debug 版本中,该代码会打印 False
  • 在 .Net Core 3.1 的 release 或 debug 版本中,该代码会打印 True

是什么导致了这种行为上的差异?(这导致我们的部分单元测试失败。)

注意:我将列表初始化放入makeList(),并关闭了内联以尝试使 .Net Core 版本与 .Net Framework 版本的工作方式相同,但无济于事。


[编辑] 正如 Hans 指出的,添加循环可以解决这个问题。

以下代码会打印 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);

但是这将打印出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);

这一定是某种奇怪的Jitter问题...


1
这个讨论是否相关? - Joe Sewell
1
当 (weakRef.IsAlive) 时 { GC.Collect(); GC.WaitForPendingFinalizers(); }。我不想猜测为什么它只执行一次这个循环 :) - Hans Passant
@JoeSewell 是的,那似乎是相关的(他们甚至在做相同类型的单元测试),除此之外,他们还尝试使用非内联方法来解决问题(但对我来说并没有用)。 - Matthew Watson
@HansPassant 这不是最奇怪的事情吗。我尝试了10次单独调用 GC.Collect(),但是并没有解决问题 - 但是一个简单的循环就可以。嗯。 - Matthew Watson
2个回答

5

仅仅因为某件事情可以被收集并不意味着它必须在尽快的时间被收集。尽管语言规定了GC可以确定一个局部变量再也不会被读取,因此不考虑它作为根节点,但这并不意味着你可以信赖一个局部变量的内容在你最后一次读取之后会立即被收集。

这不是运行时定义行为的一些变化,而是在两个运行时中的未定义行为,因此它们之间的差异是完全可以接受的。


在我看来,不要测试运行时GC是否清除了列表,而是测试您是否将变量设置为null。您知道,这是您实际控制的部分。 - Jeremy Lakeman
@JeremyLakeman 把它设置为 null 是一个无操作。在你将其设置为 null 之后,变量永远不会被读取,因此它没有任何作用。编译器甚至有权从编译代码中删除它(我不知道它是否实际上这样做,只是它可以这样做是合法的)。 - Servy
@JeremyLakeman,你不需要对垃圾回收进行单元测试。这没有任何意义。测试GC是微软的工作,而不是你的工作。你编写的任何清理代码都是专门针对由GC管理的东西,因此你不需要GC来执行任何测试。 - Servy
1
然而,GC行为的轻微更改引发了OP的问题,因为他正在测试GC。我同意,他不应该这样做。他的测试可以断言资源已被处理,任何额外操作都是不必要的。 - Jeremy Lakeman
@JeremyLakeman,这就是为什么你不应该编写依赖于GC未定义实现细节的代码。编写单元测试并不能使依赖这种行为更加合适。即使你有一些可能发现你的不可靠代码正在破坏,也不能使编写不可靠代码更加合适。 - Servy
显示剩余2条评论

0

当我移除列表变量时,我得到了要释放的引用:

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

以下测试用例失败:
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);
        }
   }
}

如果我们查看IL,我们可以看到null被分配给本地变量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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接