这个有趣的问题感谢装配脑袋友情提供。

请看如下代码:

    public class Dummy
    {
        public static Dummy Instance;
        public int X = 1;

        ~Dummy()
        {
            Instance = this;
        }
    }

通过如下代码进行调用(输出日志的地方我稍作调整):

Task.Run(() =>
{
    var d = new Dummy();
    d = null;
    GC.Collect();
    GC.WaitForFullGCComplete();

}).Wait();

var isNull = Dummy.Instance == null;
Console.WriteLine(isNull);
if (false == isNull)
{
    Console.WriteLine(Dummy.Instance.X);
}
else
{
    Console.WriteLine("Oh no!Dummy.Instance is null.");
}

问题:上述输出的Instance == null是True还是False?

此处您可以先停止阅读下面的分析,想一想您的回答会是什么呢?

首先这个题目一看就是那种明知有坑让你钻进去但是你还可能必须先钻进去的感觉。尤其是Task、GC、静态字段、实例字段,析构函数这么多东西混在一起的时候,一看就和多线程有关系,相当具有迷惑性,对不对?

我第一次看到的时候,认为Task运行起来进行GC回收然后Wait等到任务结束,变量d指向的对象因为GC.WaitForFullGCComplete()这一行,应该已经被垃圾回收成功,执行析构函数的时候,静态变量Instance指向的当前对象this(也就是变量d一开始所指向的引用对象)应该是null,那么Instance==null肯定返回True。或者输出应该总是一个确定值。

但是实际运行效果并不总是如此,请注意,经我个人多次实验,循环多次(大于等于1小于等于50000),输出True和False的次数是不确定的,但是True的出现概率明显多过False,False的总数好像总是1到10个之间。

为了防止C#编译器的某些优化,分别对比Release和Debug下的运行效果,结果还是一样的。

然后实在有点想不通为什么输出的结果有两种。循环实验了下如下代码,没有Task干扰,但效果和有Task运行的也是差不多,都有True或False输出,也就是说不用Task顺序执行GC代码也是有不同的输出。

var d = new Dummy();
d = null;
GC.Collect();
GC.WaitForFullGCComplete();

var isNull = Dummy.Instance == null;
Console.WriteLine(isNull);
if (false == isNull)
{
    Console.WriteLine(Dummy.Instance.X);
}
else
{
    Console.WriteLine("Oh no!Dummy.Instance is null.");
}

最近正好我在重新学习GC,不久前又刚刚总结了一下GC知识,想起析构函数终结上有“延长”垃圾对象生命周期的情况,但也说不通。又想过是否析构函数对静态字段进行了特殊优化,比如Instance赋值后导致GC回收策略自动调整,将G0代调整为G1代,又或者析构函数执行时this没有自动回收,也就是静态字段赋值有线程安全的控制导致先将this赋值给Instance然后this等Instance被回收才置为空,但因为Instance是静态字段,是GC的根,所以,嗯?学了很多理论,发现实践起来依然不是那么回事。

实在想不出根本原因,请教了下脑袋,他简要回答是“实际造成竞态条件的是Finalizer执行的线程。。”。

析构函数竞态条件,Finalizer,线程?哦,wait,等等,主线程、当前Task运行的线程池托管线程、GC线程、Finalizer线程,产生了竞态条件的是几种线程之间(比如GC线程和Finalizer线程)还是相同类型的线程之间(比如Finalizer线程和Finalizer线程)产生竞争呢?

顺着这个思路,把线程ID打印出来对比一下不就有结论了吗?

严重声明:这里我也不清楚执行析构函数 ~Dummy()时当前线程是否就是Finalizer线程,看书上好像是这个意思,但没给出代码,本文先暂时以Finalizer线程这么命名这个线程吧。如果您知道如何正确取得GC线程和Finalizer线程请不另赐教。

立即动手,调整了一下代码,多打印出一些日志,虽然打印出来的日志有点凌乱,但是终于可以肯定Task和析构函数执行的托管线程ID的不同,而析构函数里面的托管线程的线程ID总是一样

    public class Dummy
    {
        public static Dummy Instance;
        public int X = 1;

        public static ConcurrentBag<int> threadIDBag = new ConcurrentBag<int>();

        ~Dummy()
        {
            var threadId = Thread.CurrentThread.ManagedThreadId;
            Console.WriteLine("Destructor CurrentContext ThredID:{0}", threadId);
            if (threadIDBag.Contains(threadId) == false)
            {
                threadIDBag.Add(threadId);
            }

            Instance = this;

            //Console.WriteLine("Destructor===Instance is null:{0}", Instance == null);
        }
    }
Dummy

相关文章:

  • 2022-12-23
  • 2019-07-23
  • 2022-12-23
  • 2021-05-18
  • 2022-12-23
  • 2021-11-25
  • 2022-12-23
  • 2021-10-10
猜你喜欢
  • 2022-12-23
  • 2021-07-28
  • 2022-12-23
  • 2022-12-23
  • 2021-08-22
  • 2022-12-23
  • 2022-12-23
相关资源
相似解决方案