當心使用 Task.Run 續篇

關於前兩天發佈的文章:爲何要當心使用 Task.Run,對文中演示的示例到底會不會致使內存泄露,給不少人帶來了疑惑。這點我必須向你們道歉,是我對致使內存泄漏的緣由沒描述和解釋清楚,也沒用實際的示例證明,是個人錯。函數

可是,文中示例演示的 Task.Run 捕獲類成員的狀況,確實會有內存泄漏的風險,我將在本文演示給你們看。測試

若是一個對象(或數據)不須要再使用了,但依然還一直佔據內存空間,則視爲內存泄漏。這一點你們觀點是一致的吧,那如何來檢測對象有沒有被回收呢?code

咱們知道,在 C# 中,實例對象被釋放回收,必然會執行析構函數。因此咱們能夠對一個類重寫其析構函數,若是該類的實例對象使用完後,強制執行 GC 回收,其析構函數依然不被執行,則說明 GC 沒有回收該對象。若 GC 後面一直不回收這個對象,則說明存在內存泄漏。對象

手動強制執行 GC 回收的代碼以下:blog

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

這三句代碼能夠確保 GC 把全部能搜索到的可回收對象清理乾淨。注意:不推薦在生產環境這樣寫。內存

咱們仍是用 爲何要當心使用 Task.Run 這篇文章用到的示例,只是爲了測試稍加修改了一下:資源

class Program
{
    static void Main(string[] args)
    {
        Test();

        // 對不須要再使用的資源強制回收
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        // 程序保活
        while (true)
        {
            Thread.Sleep(100);
        }
    }

    static void Test()
    {
        var myClass = new MyClass();
        myClass.Foo();
        // 到這,myClass對象不須要再使用了
    }
}

public class MyClass
{
    private int _id;
    private List<string> _list;

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

    ~MyClass()
    {
        Console.WriteLine("MyClass instance has been colleted.");
    }
}

咱們在 myClass 對象使用完後,手動強制執行 GC 回收,運行結果以下:get

咱們看到 MyClass 的析構函數一直沒有執行,也就意味着它的實例一直沒有被回收。string

如今咱們修改 MyClass 類的 Foo 方法,改用本地(局部)變量試一試:it

...
public Task Foo()
{
    var localId = _id;
    return Task.Run(() =>
    {
        Console.WriteLine($"Task.Run is executing with ID {localId}");
    });
}
...

再運行看看效果:

此次咱們能夠看到,MyClass 的析構函數執行了,說明實例對象被回收了。

先後惟一區別是,前者在 Task.Run 的匿名方法中捕獲了類的成員,然後者使用了本地變量。前者出現了內存泄漏,後者避免了內存泄漏。

因此,在 Task.Run 的匿名方法中捕獲類的成員,確實有可能致使內存泄漏(注意是有可能而不是必定)。

那背後的緣由是什麼呢?我在上一篇文章是這樣解釋的:

私有成員 _idTask.Run 的匿名方法捕獲使用,進而致使 MyClass 實例被引用。當外部使用完 MyClass 實例時,本該由 GC 回收的時候卻發現它還被其它資源引用着,因此 GC 認爲該實例不該該被回收,也就可能永遠失去了被回收的機會。

這個解釋有很大的問題,至少給廣大讀者帶來了兩大疑惑:

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

感謝善於思考提出疑惑的讀者們,爲大家點贊。

這兩大疑惑該如何解釋?後半部分我還沒寫完,你們能夠先思考一下,我將在下一篇給你們解惑,望見諒。固然,個人解釋也不必定會是對的,但願你們帶着懷疑的態度和批判性思惟來看個人文章,也請你們分享本身的理解和觀點。

相關文章
相關標籤/搜索