关于GC和析构函数的一个趣题
这个有趣的问题感谢装配脑袋友情提供。
请看如下代码:
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和析构函数的一个趣题的更多相关文章
- [转]趣题:一个n位数平均有多少个单调区间?---- From Matrix67
考虑这么一个 14 位数 02565413989732 ,如图所示,它的数字先逐渐变大,然后开始变小,再变大,再变小,再变大,再变小.我们就说,它一共包含了 6 个单调区间.我们的问题就是:一个 n ...
- 【C#】GC和析构函数(Finalize 方法)
析构函数: (来自百度百科)析构函数(destructor) 与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),系统自动执行析构函数.析构函数往往用来做"清理善后&quo ...
- 关于php析构函数的一个有趣问题
随着面向对象编程的普遍展开,面向对象展现了其中很多有趣的问题.相信很多初学者学习php面向对象时会接触两个函数,构造函数与析构函数.构造函数似乎用的更多,析构函数用的较少(相对初学者有限编程经验而言, ...
- C 解决百度知道的一个高中题
前言 今天看见一道百度知道上提问,是这样的. 仔细算了一下, 花了30min.才整出来了,估计现在回去参加高考,数学及格都悬.有时候想做这样的题有什么用, 学这些东西有什么意义,在这种方面浪费时间有什 ...
- POJ 2260(ZOJ 1949) Error Correction 一个水题
Description A boolean matrix has the parity property when each row and each column has an even sum, ...
- c++学习笔记4,调用派生类的顺序构造和析构函数(一个)
测试源代码: //測试派生类的构造函数的调用顺序何时调用 //Fedora20 gcc version=4.8.2 #include <iostream> using namespace ...
- 一个JAVA题引发的思考
转载自:http://www.cnblogs.com/heshan664754022/archive/2013/03/24/2979495.html 十年半山 今天在论坛闲逛的时候发现了一个很有趣的题 ...
- ACM/ICPC Moscow Prefinal 2019 趣题记录
### Day1: ### **Problem C:** 设$k_i$为$[A, B]$中二进制第$i$位是1的数的个数. 给出$k_0 \cdots k_{63}$, 求出$[A, B]$ ...
- 代数&数论趣题集萃
暑假总不能只学习平面几何.所以这里也收集一些有趣的代数题或数论题,同时记下解法的一些提示.给未来的自己复习参考用. 多图片预警(请注意流量) 目录: Part 0:其他(8) Part 1:不等式(1 ...
随机推荐
- java接口
一.定义 Java接口(Interface),是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为( ...
- vim 基本使用
vim 下基本命令 重新加载 .vimrc source ~/.vimrc 列出当前缓冲区的所有文档 ls 然后使用 b+编号 移至该文档 选中多行 v + shift 然后 j k 上下移动 缩进单 ...
- 进击的Python【第五章】:Python的高级应用(二)常用模块
Python的高级应用(二)常用模块学习 本章学习要点: Python模块的定义 time &datetime模块 random模块 os模块 sys模块 shutil模块 ConfigPar ...
- Python OS模块常用函数说明
Python的标准库中的os模块包含普遍的操作系统功能.如果你希望你的程序能够与平台无关的话,这个模块是尤为重要的.即它允许一个程序在编写后不需要任何改动,也不会发生任何问题,就可以在Linux和Wi ...
- animation_Frame动画图片轮播
我们刚接触的时候想弄一个轮播图片的一个小案例,但一开始我们以为和以前写java一样,要写一下方法,逻辑:但今天你学了这个Frame动画就可以轻松搞定!下面我们来看看这个Frame是怎么实现的. 第一步 ...
- win 文字转化为语音
mshta vbscript:createobject("sapi.spvoice").speak("hello")(window.close)
- Manthan, Codefest 16(B--A Trivial Problem)
B. A Trivial Problem time limit per test 2 seconds memory limit per test 256 megabytes input standar ...
- linux kernel elv_queue_empty野指针访问内核故障定位与解决
1. 故障描述 故障操作步骤: 单板上插了一个U盘,出问题前正在通过FTP往单板上拷贝文件,拷贝的过程中单板自动重启. 故障现象: Entering kdb (current=0xc000000594 ...
- ACM: ICPC/CCPC Sudoku DFS - 数独
Sudoku Time Limit : 3000/1000ms (Java/Other) Memory Limit : 65535/65535K (Java/Other) Total Submis ...
- AFNetWorking设置HTTPRequestHeaders的坑
今天在项目中要封装一个请求头但是用如下方法总是失败: 求其原因不知道: 于是乎改用了属性对象后居然成功了..: // // RequestManager.m // 获取天气demo // // ...