當心使用 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 的分代算法。ui

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

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

alt

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

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

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

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

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

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

因爲實際狀況 Task.Run 通常爲耗時操做(非耗時任務通常沒有必要使用 Task.Run),因此時間點的順序極有多是:一、二、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內存泄漏的爭議》

相關文章
相關標籤/搜索