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

请看如下代码:

  1. public class Dummy
  2. {
  3. public static Dummy Instance;
  4. public int X = ;
  5.  
  6. ~Dummy()
  7. {
  8. Instance = this;
  9. }
  10. }

通过如下代码进行调用(输出日志的地方我稍作调整):

  1. Task.Run(() =>
  2. {
  3. var d = new Dummy();
  4. d = null;
  5. GC.Collect();
  6. GC.WaitForFullGCComplete();
  7.  
  8. }).Wait();
  9.  
  10. var isNull = Dummy.Instance == null;
  11. Console.WriteLine(isNull);
  12. if (false == isNull)
  13. {
  14. Console.WriteLine(Dummy.Instance.X);
  15. }
  16. else
  17. {
  18. Console.WriteLine("Oh no!Dummy.Instance is null.");
  19. }

问题:上述输出的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代码也是有不同的输出。

  1. var d = new Dummy();
  2. d = null;
  3. GC.Collect();
  4. GC.WaitForFullGCComplete();
  5.  
  6. var isNull = Dummy.Instance == null;
  7. Console.WriteLine(isNull);
  8. if (false == isNull)
  9. {
  10. Console.WriteLine(Dummy.Instance.X);
  11. }
  12. else
  13. {
  14. Console.WriteLine("Oh no!Dummy.Instance is null.");
  15. }

最近正好我在重新学习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总是一样

  1. public class Dummy
  2. {
  3. public static Dummy Instance;
  4. public int X = ;
  5.  
  6. public static ConcurrentBag<int> threadIDBag = new ConcurrentBag<int>();
  7.  
  8. ~Dummy()
  9. {
  10. var threadId = Thread.CurrentThread.ManagedThreadId;
  11. Console.WriteLine("Destructor CurrentContext ThredID:{0}", threadId);
  12. if (threadIDBag.Contains(threadId) == false)
  13. {
  14. threadIDBag.Add(threadId);
  15. }
  16.  
  17. Instance = this;
  18.  
  19. //Console.WriteLine("Destructor===Instance is null:{0}", Instance == null);
  20. }
  21. }

Dummy

调用代码如下:

  1. static void Main(string[] args)
  2. {
  3. var counter = ; //statistics Dummy Instance is not null count
  4. var testCnt = ;// 50000; //执行task个数
  5. while (testCnt > )
  6. {
  7. testCnt--;
  8.  
  9. Task.Run(() =>
  10. {
  11. var d = new Dummy();
  12. d = null;
  13. GC.Collect();
  14. GC.WaitForFullGCComplete();
  15.  
  16. Console.WriteLine("Task CurrentContext ThredID:{0}", Thread.CurrentThread.ManagedThreadId);
  17.  
  18. }).Wait();
  19.  
  20. var isNull = Dummy.Instance == null;
  21. Console.WriteLine(isNull);
  22. if (false == isNull)
  23. {
  24. Console.WriteLine(Dummy.Instance.X);
  25. counter++;
  26. }
  27. else
  28. {
  29. Console.WriteLine("Oh no!Dummy Instance is null.");
  30. }
  31.  
  32. Console.WriteLine("========================");
  33.  
  34. }
  35.  
  36. Thread.Sleep();
  37. Console.WriteLine("End Task......");
  38. Console.WriteLine("Dummy Instance is not null counter:{0}", counter);
  39.  
  40. Console.WriteLine("Finalizer ThreadID Count:{0}", Dummy.threadIDBag.Count); //此处输出为1
  41.  
  42. Console.ReadKey();
  43. }

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. 将 xunit.runner.dnx 的 xml 输出转换为 Nunit 格式

    由于目前 DNX 缺乏 XSLT 的转换能力,因此只能使用变通方法.具体参考这个链接 主要内容复制过来是: From @eriklarko on July 14, 2015 7:38 As a wor ...

  2. 收集常用的.net开源项目

    Json.NET http://json.codeplex.com/ Json.NET是一个读写Json效率比较高的.Net框架.Json.Net 使得在.Net环境下使用Json更加简单.通过Lin ...

  3. 学习微信小程序之css12设置盒子内容的宽高

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. AndroidAnnotations(Code Diet)android快速开发框架

    最近用了一款很不错的android快速开发框架,1000行的代码瞬间变成几百行,不用你会后悔的 特点: (1) 依赖注入:包括view,extras,系统服务,资源等等(2) 简单的线程模型,通过an ...

  5. POJ 2653 Pick-up sticks (线段相交)

    题意:给你n条线段依次放到二维平面上,问最后有哪些没与前面的线段相交,即它是顶上的线段 题解:数据弱,正向纯模拟可过 但是有一个陷阱:如果我们从后面向前枚举,找与前面哪些相交,再删除前面那些相交的线段 ...

  6. supervisor-2:event

    转载别人博客,做个记录 原文链接:http://lixcto.blog.51cto.com/4834175/1540169 supervisor的event机制其实,就是一个监控/通知的框架.抛开这个 ...

  7. ubuntu 16.04 + N驱动安装 +CUDA+Qt5 + opencv

    Nvidia driver installation(after download XX.run installation file) 1. ctrl+Alt+F1   //go to virtual ...

  8. dedecms循环列表样式

    简单用法: {dede:arclist typeid="1" row="} <li class="list[field:global.autoindex/ ...

  9. 用Fiddler的自动响应模拟系统集成

    1. 下载最新版本的Fiddler Fiddler 官网 2, 安装并启动Fiddler 3, 勾选自动响应 见上图 4, 添加自动响应规则 见上图 5, 添加自动响应内容文件 添加响应文件到Fidd ...

  10. iOS UIColor RGB HEX

    +(UIColor *)colorWithR:(CGFloat)r g:(CGFloat)g b:(CGFloat)b a:(CGFloat)a{ return [UIColor colorWithR ...