關於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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM