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

请看如下代码:

    public class Dummy
{
public static Dummy Instance;
public int X = ; ~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 = ; 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 = ; //statistics Dummy Instance is not null count
var testCnt = ;// 50000; //执行task个数
while (testCnt > )
{
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();
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

关于GC和析构函数的一个趣题的更多相关文章

  1. [转]趣题:一个n位数平均有多少个单调区间?---- From Matrix67

    考虑这么一个 14 位数 02565413989732 ,如图所示,它的数字先逐渐变大,然后开始变小,再变大,再变小,再变大,再变小.我们就说,它一共包含了 6 个单调区间.我们的问题就是:一个 n ...

  2. 【C#】GC和析构函数(Finalize 方法)

    析构函数: (来自百度百科)析构函数(destructor) 与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),系统自动执行析构函数.析构函数往往用来做"清理善后&quo ...

  3. 关于php析构函数的一个有趣问题

    随着面向对象编程的普遍展开,面向对象展现了其中很多有趣的问题.相信很多初学者学习php面向对象时会接触两个函数,构造函数与析构函数.构造函数似乎用的更多,析构函数用的较少(相对初学者有限编程经验而言, ...

  4. C 解决百度知道的一个高中题

    前言 今天看见一道百度知道上提问,是这样的. 仔细算了一下, 花了30min.才整出来了,估计现在回去参加高考,数学及格都悬.有时候想做这样的题有什么用, 学这些东西有什么意义,在这种方面浪费时间有什 ...

  5. POJ 2260(ZOJ 1949) Error Correction 一个水题

    Description A boolean matrix has the parity property when each row and each column has an even sum, ...

  6. c++学习笔记4,调用派生类的顺序构造和析构函数(一个)

    测试源代码: //測试派生类的构造函数的调用顺序何时调用 //Fedora20 gcc version=4.8.2 #include <iostream> using namespace ...

  7. 一个JAVA题引发的思考

    转载自:http://www.cnblogs.com/heshan664754022/archive/2013/03/24/2979495.html 十年半山 今天在论坛闲逛的时候发现了一个很有趣的题 ...

  8. ACM/ICPC Moscow Prefinal 2019 趣题记录

    ### Day1: ### **Problem C:** 设$k_i​$为$[A, B]​$中二进制第$i​$位是1的数的个数. 给出$k_0 \cdots k_{63}​$, 求出$[A, B]​$ ...

  9. 代数&数论趣题集萃

    暑假总不能只学习平面几何.所以这里也收集一些有趣的代数题或数论题,同时记下解法的一些提示.给未来的自己复习参考用. 多图片预警(请注意流量) 目录: Part 0:其他(8) Part 1:不等式(1 ...

随机推荐

  1. NoSql数据库初探-mongoDB环境搭建

    NoSQL数据库一改关系型数据库的缺点,更容易的集成.分布式.无模式.故障恢复等特点,正在一步步餐食关系型数据库的市场,作为一个与时俱进的码农了解一下新技术是必须的,尤其是在读了<NoSql精粹 ...

  2. maven权威指南学习笔记(三)——一个简单的maven项目

    目标: 对构建生命周期 (build  lifecycle),Maven仓库 (repositories),依赖管理 (dependency management)和项目对象模型 (Project O ...

  3. windows 物理内存获取

    由于我一般使用的虚拟内存, 有时我们需要获取到物理内存中的数据(也就是内存条中的真实数据), 按理说是很简单,打开物理内存,读取就可以了.但似乎没这么简单: #include "window ...

  4. 动画效果interpolator

      Interpolator 被用来修饰动画效果,定义动画的变化率 AccelerateDecelerateInterpolator 在动画开始与结束的地方速率改变比较慢,在中间的时候加速 Accel ...

  5. supervisor-1:基础篇

    别人博客转载,做个记录 原文链接:http://lixcto.blog.51cto.com/4834175/1539136 有阵子没写博客了,这段时间一直在研究python django框架和前端相关 ...

  6. oracle表空间简单学习

    1.重命名表空间:alter tablespace 原表空间名 rename to 新表空间名ps:(1)system 和sysaux表空间不能重名(2)如果该表空间中任何一个数据出于脱机状态或者表空 ...

  7. ruby开发环境配置

    环境:win7 64位 软件:Ruby2.2.5,devkit对应版本,rubygems,rails 一:安装Ruby 1.在这个网站:http://rubyinstaller.org/downloa ...

  8. java分享第二十天(build.xml的语法及写法)

    通常情况下,Ant构建文件build.xml应该在项目的基础目录.可以自由使用其他文件名或将构建文件中其他位置.在本练习中,创建一个名为build.xml 在电脑的任何地方的文件. <?xml ...

  9. 不用css3的响应式img(按比例缩小图片)

    有时候自己为了控制图片的响应式,按比例缩小放大图片,如果解决这个问题,用js吧,很麻烦.也会损失很大的加载运行速度等:所以我们还是用css来解决这个问题,用css来强制按比例压缩图片的高度或宽度,看代 ...

  10. 解决 adb.exe 停止工作小续

    继adb 停止工作的问题之后,又碰见了adb 停止工作的问题. 在使用adb install app.apk 之后给出错误信息如下: * daemon not running. starting it ...