2015-01-31 3 views
13

sagen, dass ich die folgende Klasse haben:Was passiert mit Aufgaben, die nie abgeschlossen werden? Sind sie richtig entsorgt?

class SomeClass 
{ 
    private TaskCompletionSource<string> _someTask; 

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

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

Dann anderswo, ich nenne

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

Die //Some more code wird nicht aufgerufen werden, bis _someTask.SetResult(..) genannt wird. Der Aufruf-Kontext wartet irgendwo im Speicher.

Allerdings wird SetResult(..) niemals aufgerufen, und wird nicht mehr referenziert und wird nicht mehr erfasst. Wird dadurch ein Speicherleck erzeugt? Oder weiß .Net automatisch, dass der Aufrufkontext entsorgt werden muss?

+0

Ein TCS ist ein isoliertes Objekt. Wenn es nicht erreichbar ist, wird es gesammelt. Seine Task verweist nicht auf das TCS, sodass Sie eine nicht kompilierbare Task ohne ein entsprechendes TCS im Speicher haben können. Wenn diese Aufgabe nicht referenziert wird, wird sie ebenfalls gesammelt. – usr

Antwort

9

Aktualisiert, ein guter Punkt von @SriramSakthivel, stellt sich heraus, ich habe bereits eine sehr ähnliche Frage beantwortet:

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

So bin ich dieses als Community Wiki markiert.

Allerdings sagen wir mal SetResult (..) nie aufgerufen wird, und someClassInstance nicht mehr Bezug genommen wird, und ist Müll gesammelt. Erstellt dies ein Speicherleck? Oder kennt .Net automatisch den Aufruf-Kontext, der entsorgt werden muss?

Wenn durch den rufenden Kontext Sie das Compiler-generierte Zustandsmaschine Objekt bedeutet (die den Zustand der async Methode darstellt), dann ja, es wird in der Tat abgeschlossen sein.

Beispiel:

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); 
    } 
} 

Ausgang:

 
Press enter to GC 

~SomeDisposable 
Press enter to exit 

IMO, dieses Verhalten ist logisch, aber es könnte noch ein wenig unerwartet, dass something trotz der Tatsache fertig gestellt wird, dass die using Bereich für es ist nie zu Ende (und daher seine SomeDisposable.Dispose wurde nie genannt) und dass die Task zurückgegeben von TestSomethingAsync ist noch am Leben und referenziert in Main.

Dies könnte zu einigen obskuren Fehlern bei der Codierung von asynchronen Dateien auf Systemebene führen. Es ist sehr wichtig, GCHandle.Alloc(callback) für alle OS-Interop-Callbacks zu verwenden, die nicht außerhalb der async Methoden referenziert werden. Doing allein am Ende der async Methode ist nicht effektiv. Ich schrieb darüber in Details hier:

Async/await, custom awaiter and garbage collector

Auf einer seitlichen Anmerkung, gibt es eine andere Art von C# Zustandsmaschine: ein Verfahren mit return yield. Interessanterweise implementiert es zusammen mit IEnumerable oder IEnumerator auch IDisposable. Unter Berufung auf seine Dispose wird entspannen alle using und finally Aussagen (auch im Fall einer unvollständigen enumerable Sequenz):

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" 

Im Gegensatz zu diesem, mit async Methoden gibt es keine direkte Möglichkeit, das unwiding von using und finally steuern.

+0

Woher wissen Sie * "das Compiler-generierte State Machine-Objekt" * wird garantiert, wenn das Objekt finalisiert wird, obwohl? –

+0

@ BlueRaja-DannyPflughoef, was meinst du unter "entsorgung" hier? Das Zustandsmaschineobjekt implementiert nicht IDisposable. Es ist nur eine Boxed 'struct', aber alle Felder (wie' something' oben) werden durch Garbage-Collection gesammelt und finalisiert (beachten Sie, wie '~ SomeDisposable' aufgerufen wird) - weil die Struktur selbst GC'ed wird. – Noseratio

+0

@SriramSakthivel, danke für deinen Punkt über ein Duplikat, ich habe dieses als Wiki markiert. – Noseratio

6

You should ensure your tasks are always completed.

Im normalen Fall wird der "Andere Code, der SetResult aufruft" irgendwo als Callback registriert. Wenn z. B. nicht verwaltete überlappende E/A verwendet wird, ist diese Rückrufmethode ein GC-Root. Dann hält dieser Callback explizit _someTask am Leben, was seinen Task am Leben erhält, der den Delegierten für //Some more code am Leben erhält.

Wenn die „Andere Code, der SetResult ruft“ ist nicht (direkt oder indirekt) als Rückruf registriert, dann kann ich nicht denken wird es ein Leck sein. Beachten Sie, dass dies kein unterstützter Anwendungsfall ist. Dies kann nicht garantiert werden. Aber ich habe einen Speicherprofilerstellungstest mit dem Code in Ihrer Frage erstellt und er scheint nicht zu lecken.

+0

Also hängt es davon ab, wo der "andere Code" ist? Ich bin verwirrt darüber, wie man weiß, welche Fälle auslaufen ... Mein Anwendungsfall ist, dass 'WaitForThing()' auf Benutzereingaben von einem Benutzer am anderen Ende eines IM-Clients * wartet (und von dem Objekt aufgerufen wird, das stellt einen "Freund" dar * und gibt es zurück. Wenn dieser Benutzer jedoch als Freund entfernt wird, kann diese Eingabe niemals stattfinden - es gibt keine Möglichkeit für mich, sie zu "vervollständigen". –

+1

@ BlueRaja-DannyPflughoef: Ich empfehle, die Aufgabe als abgebrochen ('SetCanceled') zu beenden, wenn der Benutzer als Freund entfernt wird. –