Wird es als akzeptabel angesehen, Dispose () nicht für ein TPL-Task-Objekt aufzurufen?

123

Ich möchte eine Aufgabe auslösen, die in einem Hintergrundthread ausgeführt wird. Ich möchte nicht auf die Fertigstellung der Aufgaben warten.

In .net 3.5 hätte ich Folgendes getan:

ThreadPool.QueueUserWorkItem(d => { DoSomething(); });

In .net 4 ist die TPL der vorgeschlagene Weg. Das übliche Muster, das ich empfohlen habe, ist:

Task.Factory.StartNew(() => { DoSomething(); });

Die StartNew()Methode gibt jedoch ein TaskObjekt zurück, das implementiert wird IDisposable. Dies scheint von Leuten übersehen zu werden, die dieses Muster empfehlen. In der MSDN-Dokumentation zur Task.Dispose()Methode heißt es:

"Rufen Sie Dispose immer an, bevor Sie Ihren letzten Verweis auf die Aufgabe freigeben."

Sie können dispose für eine Aufgabe erst dann aufrufen, wenn sie abgeschlossen ist. Wenn Sie also den Hauptthread warten und dispose aufrufen, wird der Sinn eines Hintergrundthreads in erster Linie zunichte gemacht. Es scheint auch kein abgeschlossenes / abgeschlossenes Ereignis zu geben, das für die Bereinigung verwendet werden könnte.

Die MSDN-Seite in der Task-Klasse kommentiert dies nicht, und das Buch "Pro C # 2010 ..." empfiehlt dasselbe Muster und kommentiert die Entsorgung von Aufgaben nicht.

Ich weiß, wenn ich es einfach lasse, wird der Finalizer es am Ende fangen, aber wird das zurückkommen und mich beißen, wenn ich viele Feuer mache und Aufgaben wie diese vergesse und der Finalizer-Thread überfordert ist?

Meine Fragen sind also:

  • Ist es in diesem Fall akzeptabel, Dispose()die TaskKlasse nicht anzurufen ? Und wenn ja, warum und gibt es Risiken / Konsequenzen?
  • Gibt es eine Dokumentation, die dies diskutiert?
  • Oder gibt es eine geeignete Möglichkeit, das TaskObjekt, das ich verpasst habe , zu entsorgen ?
  • Oder gibt es eine andere Möglichkeit, Fire & Forget-Aufgaben mit der TPL auszuführen?
Simon P Stevens
quelle
1
Verwandte: Der richtige Weg zu Feuer und Vergessen (siehe Antwort )
Simon P Stevens

Antworten:

108

Es gibt eine Diskussion darüber in den MSDN - Foren .

Stephen Toub, ein Mitglied des Microsoft Pfx-Teams, hat Folgendes zu sagen:

Task.Dispose liegt vor, weil Task möglicherweise ein Ereignishandle umschließt, das beim Warten auf den Abschluss der Aufgabe verwendet wird, falls der wartende Thread tatsächlich blockieren muss (im Gegensatz zum Drehen oder potenziellen Ausführen der Aufgabe, auf die er wartet). Wenn Sie nur Fortsetzungen verwenden, wird dieses Ereignishandle niemals zugewiesen
.
Es ist wahrscheinlich besser, sich auf die Finalisierung zu verlassen, um die Dinge zu erledigen .

Update (Okt 2012)
Stephen Toub hat einen Blog mit dem Titel Muss ich Aufgaben entsorgen? Dies gibt einige Details und erklärt die Verbesserungen in .Net 4.5.

Zusammenfassend: Sie müssen TaskObjekte nicht 99% der Zeit entsorgen .

Es gibt zwei Hauptgründe, ein Objekt zu entsorgen: nicht verwaltete Ressourcen rechtzeitig und deterministisch freizugeben und die Kosten für die Ausführung des Finalizers des Objekts zu vermeiden. Beides gilt Taskmeistens nicht für:

  1. Ab .Net 4.5 Taskwird das interne Wartehandle (die einzige nicht verwaltete Ressource im TaskObjekt) nur dann zugewiesen, wenn Sie explizit das IAsyncResult.AsyncWaitHandlevon Task, und verwenden
  2. Das TaskObjekt selbst hat keinen Finalizer. Das Handle selbst ist in ein Objekt mit einem Finalizer eingeschlossen. Wenn es nicht zugewiesen ist, kann kein Finalizer ausgeführt werden.
Kirill Muzykov
quelle
3
Danke, interessant. Es widerspricht jedoch der MSDN-Dokumentation. Gibt es ein offizielles Wort von MS oder dem .net-Team, dass dies ein akzeptabler Code ist? Es gibt auch den Punkt am Ende dieser Diskussion, dass "was ist, wenn sich die Implementierung in einer zukünftigen Version ändert"
Simon P Stevens
Eigentlich habe ich gerade bemerkt, dass der Antwortende in diesem Thread tatsächlich bei Microsoft arbeitet, anscheinend im pfx-Team, also denke ich, dass dies eine Art offizielle Antwort ist. Aber es gibt Hinweise darauf, dass dies nicht in allen Fällen funktioniert. Wenn es ein potenzielles Leck gibt, kann ich besser einfach zu ThreadPool.QueueUserWorkItem zurückkehren, von dem ich weiß, dass es sicher ist?
Simon P Stevens
Ja, es ist sehr seltsam, dass es eine Dispose gibt, die Sie möglicherweise nicht anrufen. Wenn Sie sich die Beispiele hier msdn.microsoft.com/en-us/library/dd537610.aspx und hier msdn.microsoft.com/en-us/library/dd537609.aspx ansehen , verfügen sie nicht über Aufgaben. Codebeispiele in MSDN zeigen jedoch manchmal sehr schlechte Techniken. Auch der auf die Frage beantwortete Typ arbeitet für Microsoft.
Kirill Muzykov
2
@Simon: (1) Das von Ihnen zitierte MSDN-Dokument ist ein allgemeiner Ratschlag. In bestimmten Fällen gibt es spezifischere Ratschläge (z. B. keine Verwendung EndInvokein WinForms, wenn BeginInvokeCode auf dem UI-Thread ausgeführt wird). (2) Stephen Toub ist als regelmäßiger Redner über die effektive Nutzung von PFX (z. B. auf channel9.msdn.com ) ziemlich bekannt. Wenn also jemand eine gute Anleitung geben kann, dann ist er es. Beachten Sie seinen zweiten Absatz: Es ist manchmal besser, die Dinge dem Finalisten zu überlassen.
Richard
12

Dies ist das gleiche Problem wie bei der Thread-Klasse. Es verbraucht 5 Betriebssystem-Handles, implementiert jedoch kein IDisposable. Gute Entscheidung der ursprünglichen Designer, es gibt natürlich nur wenige vernünftige Möglichkeiten, die Dispose () -Methode aufzurufen. Sie müssten zuerst Join () aufrufen.

Die Task-Klasse fügt diesem einen Handle hinzu, ein internes Ereignis zum manuellen Zurücksetzen. Welches ist die billigste Betriebssystemressource, die es gibt. Natürlich kann die Dispose () -Methode nur dieses eine Ereignishandle freigeben, nicht die 5 Handles, die Thread verwendet. Ja, mach dir keine Sorgen .

Beachten Sie, dass Sie an der IsFaulted-Eigenschaft der Aufgabe interessiert sein sollten. Es ist ein ziemlich hässliches Thema, mehr darüber können Sie in diesem Artikel der MSDN Library lesen . Sobald Sie damit richtig umgehen, sollten Sie auch einen guten Platz in Ihrem Code haben, um die Aufgaben zu entsorgen.

Hans Passant
quelle
6
Eine Aufgabe erstellt jedoch Threadin den meisten Fällen keine , sondern verwendet den ThreadPool.
Svick
-1

Ich würde gerne sehen, wie jemand die in diesem Beitrag gezeigte Technik abwägt : Typasicherer asynchroner Aufruf von Feuer und Vergessen in C #

Es sieht so aus, als würde eine einfache Erweiterungsmethode alle trivialen Fälle der Interaktion mit den Aufgaben behandeln und in der Lage sein, dispose darauf aufzurufen.

public static void FireAndForget<T>(this Action<T> act,T arg1)
{
    var tsk = Task.Factory.StartNew( ()=> act(arg1),
                                     TaskCreationOptions.LongRunning);
    tsk.ContinueWith(cnt => cnt.Dispose());
}
Chris Marisic
quelle
3
Natürlich wird die von zurückgegebene TaskInstanz nicht entsorgt ContinueWith, aber das Zitat von Stephen Toub ist die akzeptierte Antwort: Es gibt nichts zu entsorgen, wenn nichts eine blockierende Wartezeit für eine Aufgabe ausführt.
Richard
1
Wie Richard erwähnt, gibt ContinueWith (...) auch ein zweites Task-Objekt zurück, das dann nicht entsorgt wird.
Simon P Stevens
1
So wie es aussieht, ist der ContinueWith-Code tatsächlich schlechter als redudant, da dadurch eine weitere Aufgabe erstellt wird, nur um die alte Aufgabe zu entsorgen. So wie es aussieht, wäre es im Grunde nicht möglich, eine Blockierungswartezeit in diesen Codeblock einzuführen, außer wenn der von Ihnen übergebene Aktionsdelegierte versucht hätte, die Aufgaben selbst zu manipulieren, auch korrekt?
Chris Marisic
1
Möglicherweise können Sie verwenden, wie Lambdas Variablen auf etwas knifflige Weise erfassen, um die zweite Aufgabe zu erledigen. Task disper = null; disper = tsk.ContinueWith(cnt => { cnt.Dispose(); disper.Dispose(); });
Gideon Engelberth
@GideonEngelberth das müsste wohl funktionieren. Da disper niemals vom GC entsorgt werden sollte, sollte es gültig bleiben, bis sich das Lambda zur Entsorgung aufruft, vorausgesetzt, die Referenz ist dort noch gültig / Achselzucken. Benötigt man vielleicht einen leeren Versuch / Fang?
Chris Marisic