2015-01-31 27 views
13

說我有下面的類:未完成的任務會發生什麼?他們妥善處理?

class SomeClass 
{ 
    private TaskCompletionSource<string> _someTask; 

    public Task<string> WaitForThing() 
    { 
     _someTask = new TaskCompletionSource<string>(); 
     return _someTask.Task; 
    } 

    //Other code which calls _someTask.SetResult(..); 
} 

然後在其他地方,我叫

//Some code.. 
await someClassInstance.WaitForThing(); 
//Some more code 

//Some more code不會被調用,直到_someTask.SetResult(..)被調用。調用上下文在某處存儲在內存中。

但是,假設從未調用SetResult(..),並且someClassInstance停止被引用並且被垃圾收集。這是否會造成內存泄漏?或者.Net自動奇蹟般地知道調用環境需要處理?

+0

TCS是一個孤立的對象。無法訪問時收集。其任務沒有引用TCS,因此您可以在內存中沒有任何相應的TCS的情況下執行不可完成的任務。如果該任務不被引用,它也會被收集。 – usr 2015-02-01 09:48:04

回答

9

更新,好點的@SriramSakthivel,原來我已經回答了一個非常類似的問題:

Why does GC collects my object when I have a reference to it?

所以我標記這個作爲一個社區的wiki。

但是,讓我們說的setResult(..)不會被調用,並 someClassInstance停止被引用,並進行垃圾回收。 這是否會造成內存泄漏?或.Net自動神奇地知道 調用上下文需要處置?

如果由調用上下文你指的是編譯器生成的狀態機對象(代表async方法的狀態),然後是的,這的確會敲定。

例子:

static void Main(string[] args) 
{ 
    var task = TestSomethingAsync(); 
    Console.WriteLine("Press enter to GC"); 
    Console.ReadLine(); 
    GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true); 
    GC.WaitForFullGCComplete(); 
    GC.WaitForPendingFinalizers(); 
    Console.WriteLine("Press enter to exit"); 
    Console.ReadLine(); 
} 

static async Task TestSomethingAsync() 
{ 
    using (var something = new SomeDisposable()) 
    { 
     await something.WaitForThingAsync(); 
    } 
} 

class SomeDisposable : IDisposable 
{ 
    readonly TaskCompletionSource<string> _tcs = new TaskCompletionSource<string>(); 

    ~SomeDisposable() 
    { 
     Console.WriteLine("~SomeDisposable"); 
    } 

    public Task<string> WaitForThingAsync() 
    { 
     return _tcs.Task; 
    } 

    public void Dispose() 
    { 
     Console.WriteLine("SomeDisposable.Dispose"); 
     GC.SuppressFinalize(this); 
    } 
} 

輸出:

 
Press enter to GC 

~SomeDisposable 
Press enter to exit 

IMO,這種行爲是合乎邏輯的,但它仍然可能是一個有點意外的是something被最終確定,儘管事實它的範圍從未結束(因此它的SomeDisposable.Dispose從未被稱爲)並且由TestSomethingAsync返回的10個仍然存在並且在Main中被引用。

編碼系統級別的異步內容時,這可能會導致一些模糊的錯誤。在async方法之外的任何OS互操作回調中使用GCHandle.Alloc(callback)都非常重要。在async方法的末尾單獨做GC.KeepAlive(callback)是無效的。我在細節寫這個位置:

Async/await, custom awaiter and garbage collector

在一個側面說明,還有另一種C#狀態機:與return yield的方法。有趣的是,與IEnumerableIEnumerator一起,它也實現了IDisposable。調用其Dispose會展開任何usingfinally語句(即使不完全枚舉序列的情況下):

static IEnumerator SomethingEnumerable() 
{ 
    using (var disposable = new SomeDisposable()) 
    { 
     try 
     { 
      Console.WriteLine("Step 1"); 
      yield return null; 
      Console.WriteLine("Step 2"); 
      yield return null; 
      Console.WriteLine("Step 3"); 
      yield return null; 
     } 
     finally 
     { 
      Console.WriteLine("Finally"); 
     } 
    } 
} 
// ... 
var something = SomethingEnumerable(); 
something.MoveNext(); // prints "Step 1" 
var disposable = (IDisposable)something; 
disposable.Dispose(); // prints "Finally", "SomeDisposable.Dispose" 

與此不同,與async方法有控制的usingfinally的unwiding的直接方式。

+0

您如何知道*當編譯器生成的狀態機對象*被保證在對象完成時處置? – 2015-02-01 07:04:00

+0

@ BlueRaja-DannyPflughoeft,這裏「處置」下的意思是什麼?狀態機對象不實現'IDisposable'。它只是一個盒裝的'struct',但是它的所有字段(如上面的'something')都會被垃圾收集並最終化(注意'SomeDisposable'被調用) - 因爲結構本身被GC化了。 – Noseratio 2015-02-01 07:49:04

+0

@SriramSakthivel,感謝您對重複的觀點,我將這一個標記爲wiki。 – Noseratio 2015-02-02 05:00:25

6

You should ensure your tasks are always completed

在通常情況下,「調用SetResult的其他代碼」被註冊爲某處的回調。例如,如果它使用非託管重疊I/O,那麼該回調方法就是一個GC根。然後該回調明確保持_someTask活着,這保持其Task活着,這保持//Some more code的代表活着。

如果「呼籲其他的setResult碼」是未註冊爲回調(直接或間接),那麼我不認爲會有泄漏。請注意,這不是受支持的用例,因此不能保證。但我確實使用你的問題中的代碼創建了一個內存分析測試,並且它看起來沒有泄漏。

+0

那麼它取決於「其他代碼」在哪裏?我對如何判斷哪些情況會泄漏感到困惑......我的用例是WaitForThing()等待來自IM客戶端另一端的用戶的用戶輸入*(並且從對象代表「朋友」)*並將其返回。但是,如果該用戶作爲朋友被移除,則該輸入永遠不會發生 - 我無法「完成」該輸入。 – 2015-02-01 00:25:50

+1

@ BlueRaja-DannyPflughoeft:我建議您在將用戶作爲朋友移除時,完成取消任務('SetCanceled')。 – 2015-02-01 00:52:07