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

请看如下代码:

    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. ctf汇总

    IDF实验室:牛刀小试 IDF实验室:倒行逆施 linux shell 常用指令 汇编笔记 堆栈溢出

  2. TPC-H

    http://blog.csdn.net/leixingbang1989/article/details/8766047

  3. 【字符串匹配】KMP算法和next数组的c/c++实现

    KMP算法基本思想有许多博客都写到了,写得也十分形象,不懂得可以参考下面的传送门,我就不解释基本思想了.本文主要给出KMP算法及next数组的计算方法(主要是很多网上的代码本人(相信应该是许多人吧)看 ...

  4. java 日期格式

  5. iOS APP可执行文件的组成

    iOS APP编译后,除了一些资源文件,剩下的就是一个可执行文件,有时候项目大了,引入的库多了,可执行文件很大,想知道这个可执行文件的构成是怎样,里面的内容都是些什么,哪些库占用空间较高,可以用以下方 ...

  6. mac homebrew的用法

    与 MacPorts 类似,OS X 下还有款包管理工具为 Homebrew,安装方法也很简单. ruby -e "$(curl -fsSL https://raw.github.com/H ...

  7. android studio乱码

    http://www.cnblogs.com/Kennytian/p/4449878.html Android Studio中的乱码分好几种,一是IDE的不同窗口里显示乱码,如:logcat筛选框,S ...

  8. XVI Open Cup named after E.V. Pankratiev. GP of Ukraine

    A. Associated Vertices 首先求出SCC然后缩点,第一次求出每个点能到的点集,第二次收集这些点集即可,用bitset加速,时间复杂度$O(\frac{nm}{64})$. #inc ...

  9. 搭建ssm的领悟

    今天搭建了ssm,但是一直报错误 "Cannot load JDBC driver class 'com.mysql.jdbc.Driver" 我以为是版本的问题就换,以为是路径加 ...

  10. Android 腾讯入门教程( 智能手表UI设计 和 MVC模式 )

    *****注意到mvc 在android 中是如何进行分层分域执行各自的功能.**** 官方推荐的按钮尺寸是48像素 前端之Android入门(1):环境配置 前端之Android入门(2):程序目录 ...