WeakReference 在 .Net Framework 和 .Net Core 之间的行为不同

WeakReference behaves differently between .Net Framework and .Net Core

提问人:Matthew Watson 提问时间:8/20/2020 最后编辑:Matthew Watson 更新时间:9/3/2020 访问量:461

问:

请考虑以下代码:

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 上发布或调试版本时,该 代码打印。False
  • 在 .Net 上发布或调试生成 核心 3.1,该代码打印 .True

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

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


[编辑]正如 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);

一定是某种奇怪的抖动......

C# 弱引用 net-core-3.1 net-4.8

评论

1赞 Joe Sewell 8/20/2020
这个讨论有意义吗
1赞 Hans Passant 8/20/2020
while (weakRef.IsAlive) { GC.Collect(); GC.WaitForPendingFinalizers(); }.我真的不想猜测为什么它只经过一次循环:)
0赞 Matthew Watson 8/20/2020
@JoeSewell 是的,这似乎是相关的(他们甚至在做相同类型的单元测试),除了他们还尝试使用一种非内联方法为他们修复了它(对我来说不是)。
0赞 Matthew Watson 8/20/2020
@HansPassant 这不是最奇怪的事情吗?我尝试添加 10 个单独的调用,但这并不能解决它 - 但一个循环确实如此。嗯。GC.Collect()

答:

5赞 Servy 8/20/2020 #1

仅仅因为允许收集某些东西并不意味着它有义务尽快收集。虽然该语言指出,GC 可以确定一个局部变量永远不会被再次读取,因此不将其视为根,但这并不意味着您可以依赖在上次读取局部变量后立即收集局部变量的内容。

这不是运行时中定义的行为之间的某些更改,这是两个运行时中未定义的行为,因此它们之间的差异是完全可以接受的。

评论

0赞 Matthew Watson 8/20/2020
我同意,因此我们失败的单元测试依赖于未定义的行为。我们将不得不找到另一种方法来检查事情是否正确处理。
0赞 Jeremy Lakeman 9/3/2020
恕我直言,不要测试运行时 GC 是否为列表,测试您是否已将变量设置为 null。你知道,你实际上可以控制的那一点。
0赞 Servy 9/4/2020
@JeremyLakeman 将其设置为 null 是 no-op。将变量设置为 null 后,永远不会读取该变量,因此它不会执行任何操作。编译器甚至有权从编译的代码中删除它(我不知道它是否真的这样做,只是这样做是合法的)。
0赞 Jeremy Lakeman 9/4/2020
这仅适用于即将超出范围的局部变量。编写一个关于局部变量的单元测试没有多大意义,但你可以写一个来演示你的释放代码是否有效。这就是您可能一直在使用弱参考来证明 GC 也有效的地方。
1赞 Jeremy Lakeman 9/4/2020
然而,GC 行为的微小变化引发了 OP 的问题,因为测试 GC 正是他正在做的事情。我同意,他不应该。他的测试可以断言资源已被处置,任何更多都是不必要的。
0赞 Andreas Synnerdahl 9/3/2020 #2

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

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