關於前兩天發佈的文章:爲何要當心使用 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
的匿名方法中捕獲類的成員,確實有可能致使內存泄漏(注意是有可能而不是必定)。
那背後的緣由是什麼呢?我在上一篇文章是這樣解釋的:
私有成員
_id
被Task.Run
的匿名方法捕獲使用,進而致使MyClass
實例被引用。當外部使用完MyClass
實例時,本該由 GC 回收的時候卻發現它還被其它資源引用着,因此 GC 認爲該實例不該該被回收,也就可能永遠失去了被回收的機會。
這個解釋有很大的問題,至少給廣大讀者帶來了兩大疑惑:
_id
改成引用類型(如 String),那二者指向的就是同一個對象值,那是否是意味着即使使用本地變量也仍是沒法避免內存泄漏的問題?myClass
實例存在被捕獲的成員,則認爲它不該該被回收。那當 Task.Run
執行完後, 被捕獲的成員也使用完了,GC 再次搜索時不就能夠回收 myClass
對象嗎?只是晚了一些時間回收而已嘛。感謝善於思考提出疑惑的讀者們,爲大家點贊。
這兩大疑惑該如何解釋?後半部分我還沒寫完,你們能夠先思考一下,我將在下一篇給你們解惑,望見諒。固然,個人解釋也不必定會是對的,但願你們帶着懷疑的態度和批判性思惟來看個人文章,也請你們分享本身的理解和觀點。