专注收集记录技术开发学习笔记、技术难点、解决方案
网站信息搜索 >> 请输入关键词:
您当前的位置: 首页 > CLR

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

发布时间:2011-06-23 13:51:22 文章来源:www.iduyao.cn 采编人员:星星草
关于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

友情提示:
信息收集于互联网,如果您发现错误或造成侵权,请及时通知本站更正或删除,具体联系方式见页面底部联系我们,谢谢。

其他相似内容:

  • [精粹]正则表达式30分钟入门教程(转)

    [精华]正则表达式30分钟入门教程(转) 前言   今天做东西的时候碰到个正则表达式的需求,以前做数据验证的时候因为都是一些通用的东...

  • WCF学习1

    WCF学习一 在阅读博客园 WCF开发实战系列一:创建第一个WCF服务 一文中,发现手动配置App.config还是有难度。这篇文章没讲很多...

  • .NET 类库研究必备参照 添加微软企业库源码

    .NET 类库研究必备参考 添加微软企业库源码 前不久,为大家提供了一个.NET 类库参考源码的网站,扣丁格鲁(谐音&ldquo;coding guru...

  • .NET 类库研究必备参照 扣丁格鲁

    .NET 类库研究必备参考 扣丁格鲁 .NET 类库的强大让我们很轻松的解决常见问题,作为一个好专研的程序员,为了更上一层楼,研究CLR...

  • 垃圾回收机制GC知识再总结兼谈怎么用好GC

    垃圾回收机制GC知识再总结兼谈如何用好GC 一、为什么需要GC 应用程序对资源操作,通常简单分为以下几个步骤: 1、为对应的资源分配内...

  • .NET 4.5.1 参照源码索引

    .NET 4.5.1 参考源码索引...

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

    关于GC和析构函数的一个趣题 这个有趣的问题感谢装配脑袋友情提供。 请看如下代码: public class Dummy { publi...

  • 对程序集的几点懂得

    对程序集的几点理解   CLR对程序集的解释是:程序集是一个或多个类型定义文件及资源文件的集合。平时我们常见的后缀为dll或exe的...

  • 渣滓回收期算法简介

    垃圾回收期算法简介 垃圾回收器检查托管堆中是否有应用程序不再使用的对象,如果有,他们使用的内存就可以回收(如果一次垃圾回收之后...

  • CLR值类型跟引用类型

    CLR值类型和引用类型 知识点:引用类型、值类型、装箱、拆箱 CLR支持两种类型:引用类型和值类型。引用类型在堆上分配内存,值类型在线...