小心使用 Task.Run 解惑篇

上一篇文章之後,這篇文章主要解答以下兩個疑惑:

  1. 由於值類型是拷貝的方式賦值,所以捕獲的本地變量和類成員是指向的是各自的值,對本地變量的捕獲不會影響到整個類。但如果把 _id 改爲引用類型(如 StringBuilder),那兩者指向的就是同一個對象值,那是不是意味着即便使用本地變量也還是無法避免內存泄漏的問題?
  2. GC 第一次回收時發現 myClass 實例存在被捕獲的成員,則認爲它不應該被回收。那當 Task.Run 執行完後,GC 再次搜索時不就可以回收 myClass 對象嗎?只是晚了一些時間回收而已。

爲了方便理解,我再把昨天的關鍵代碼貼出來:

public class MyClass
{
    private int _id;

    public Task Foo()
    {
        var localId = _id;
        return Task.Run(() =>
        {
            Console.WriteLine($"Task.Run is executing with ID {localId}");
            Thread.Sleep(100); // 模擬耗時操作
        });
    }
}

先來看第一個疑惑。經實測,把 _id 改爲 StringBuilder 類型運行結果是和 int 一樣的,說明和值類型或引用類型無關。我的理解是這樣的:

我們知道,引用類型的變量在聲明的時候就會在棧中分配一個空間,用來存放地址引用,而給它的賦值則存儲在託管堆中。雖然本地變量 localId 和類的成員 _id 的地址都指向的是託管堆中同一塊空間,但他們在棧中的地址卻分屬不同的作用域。所謂被捕獲就是被作用域捕獲,當一個作用域結束時,該作用域內的成員的地址空間都會隨着一起被釋放。至於地址指向的託管堆中的字符串值,則不是作用域關心的事情。當該字符串值所在的空間沒有地址指向它時,就會被 GC 回收。 有點抽象,但應該還好理解。

再來看第二個疑惑。在此之前,我們先來了解一下 GC 的分代算法。

當 CLR 試圖搜索不再使用的對象的時,它需要遍歷託管堆上的對象。隨着程序的持續運行,託管堆可能越來越大,如果要對整個託管堆進行垃圾回收,勢必會嚴重影響性能。所以,爲了優化這個過程,CLR 中使用了分代算法

簡單來說,分代算法就是把內存中的資源劃分爲三代:Gen 0、Gen 1、Gen 2,它們被 GC 遍歷的頻率依次從高到低。所有新創建的對象屬於 Gen 0,GC 掃描它的頻率最高。進行一次掃描後,處於 Gen 0 的不可回收對象就會被標記爲 Gen 1。類似的,GC 掃描 Gen 1 時,如果 Gen 1 的對象依然不可回收,就會標記爲 Gen 2。有點像馬太效應,資源停留在內存時間越長,就越不容易被回收。

alt

Gen 2 的回收被稱爲 Full GC。而 Full GC 只有在滿足一定的條件纔會執行,具體請閱讀這篇官方文檔:

https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/notifications#full-garbage-collection

也就是說,進入 Gen 2 的資源,若條件沒有達到,就會一直不被回收。

理解了分代算法和 Full GC,第二個疑惑就迎刃而解了。第二個疑惑關鍵在三個時間點上:

  1. myClass 對象作用域結束的時間點
  2. GC 執行回收的時間點
  3. Task.Run 匿名方法執行完成的時間點

如果程序執行的時間點順序是:1、3、2,那麼不會有內存漏泄的問題,這點很容易理解。

由於實際情況 Task.Run 一般爲耗時操作(非耗時任務一般沒有必要使用 Task.Run),所以時間點的順序極有可能是:1、2、3。如果是此執行的順序,那麼 GC 在回收時就會因爲 myClass 對象存在成員被引用而把它標記爲 Gen 1。如果 Task.Run 耗時足夠長, myClass 就可能會進入 Gen 2,進而可能很難被回收,甚至可能永遠不被回收。

其實大部分場景,我們也不必過於小心,即使在 Task.Run 匿名方法捕獲了類的成員使該類的實例進入了 Gen 2,Gen 2 中留存的不再使用的資源也是有限的。根據官方文檔對 Full GC 的介紹(地址在前文),當 Gen 2 積累到一定的量時便滿足了執行回收的條件,在 GC 下一次回收時便會回收 Gen 2 中不再使用的資源。當然,作爲一個優秀的程序員,我們還是得養成好的編碼習慣,不要在 Task.Run 中的匿名方法捕獲類的成員。

最後,鄭重聲明,最近三篇關於小心使用 Task.Run 的文章皆屬我個人理解,知識水平有限,難免存在遺漏和錯誤。若有發現,請大家不吝指正。

PS:本人博客園文章一般晚於公衆號一天發佈,望大家見諒。關於是否屬於內存泄漏問題,我在今天的文章中有討論:《.NET內存泄漏的爭議》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章