关于GC和析构函数的一个趣题

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

请看如下代码:

    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

调用代码如下:

static void Main(string[] args)
{
    var counter = 0; //statistics Dummy Instance is not null count
    var testCnt = 1;// 50000; //执行task个数
    while (testCnt > 0)
    {
        testCnt--;

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

            Console.WriteLine("Task CurrentContext ThredID:{0}", Thread.CurrentThread.ManagedThreadId);

        }).Wait();

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

        Console.WriteLine("========================");

    }

    Thread.Sleep(2000);
    Console.WriteLine("End Task......");
    Console.WriteLine("Dummy Instance is not null counter:{0}", counter);

    Console.WriteLine("Finalizer ThreadID Count:{0}", Dummy.threadIDBag.Count); //此处输出为1

    Console.ReadKey();
}
RunTask

到这里我敢肯定装配脑袋说的“竞态条件”肯定不是Finalizer线程和Finalizer线程之间产生的竞态,也不是GC线程和Finalizer线程之间产生的竞态。

又因为脑袋说过Task运行后进行了Wait,应该也不是Task运行所分配的托管线程和Finalizer线程之间产生的竞态。

所以,应该是执行调用线程(本例即执行完Task后调用Console.WriteLine()的主线程)和Finalizer线程之间产生了线程竞争。

到这里能够得出的结论,我认为可能说得通的解释就是,应用程序执行线程MainThread运行代码Console.WriteLine(Dummy.Instance == null)的时候,析构函数线程FinalizerThread可能刚要执行但是还没有运行Instance=this这行代码,这样Dummy.Instance就不是空,输出就是False。

简单理解就是Finalizer线程的执行不确定性导致输出有不同效果。

不知各位以为然否?

补充三个问题:

1、如果将GC.WaitForFullGCComplete()改为GC.WaitForPendingFinalizers()输出效果如何?

2、如Dummy继承自IDisposable,执行Dispose()方法的线程ID是什么?

3、如何直接而正确取得GC线程和Finalizer线程?它们都是线程池中的托管线程吗?

多看多想再勤动手,实践出真知。

 

参考:

<<CLR Via C#>>

http://www.cppblog.com/Solstice/archive/2010/01/28/dtor_meets_threads.html

http://msdn.microsoft.com/zh-cn/library/system.idisposable.dispose%28v=vs.110%29.aspx

http://blogs.msdn.com/b/dotnet/archive/2014/11/12/net-core-is-open-source.aspx

posted on 2014-12-28 23:45  JeffWong  阅读(2926)  评论(1编辑  收藏  举报