Est-il considéré comme acceptable de ne pas appeler Dispose () sur un objet TPL Task?

123

Je souhaite déclencher une tâche à exécuter sur un thread d'arrière-plan. Je ne veux pas attendre la fin des tâches.

Dans .net 3.5, j'aurais fait ceci:

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

Dans .net 4, le TPL est la méthode suggérée. Le modèle commun que j'ai vu recommandé est:

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

Cependant, la StartNew()méthode retourne un Taskobjet qui implémente IDisposable. Cela semble être négligé par les personnes qui recommandent ce modèle. La documentation MSDN sur la Task.Dispose()méthode dit:

"Appelez toujours Dispose avant de libérer votre dernière référence à la tâche."

Vous ne pouvez pas appeler dispose sur une tâche jusqu'à ce qu'elle soit terminée, donc avoir le thread principal attendre et appeler dispose annulerait le point de faire sur un thread d'arrière-plan en premier lieu. Il ne semble pas non plus y avoir d'événement terminé / terminé qui pourrait être utilisé pour le nettoyage.

La page MSDN sur la classe Task ne commente pas cela, et le livre "Pro C # 2010 ..." recommande le même modèle et ne fait aucun commentaire sur l'élimination des tâches.

Je sais que si je le laisse simplement, le finaliseur l'attrapera à la fin, mais est-ce que cela va revenir et me mordre quand je fais beaucoup de feu et que j'oublie des tâches comme celle-ci et que le fil du finaliseur sera débordé?

Donc mes questions sont:

  • Est-il acceptable de ne pas faire appel Dispose()à la Taskclasse dans ce cas? Et si oui, pourquoi et y a-t-il des risques / conséquences?
  • Y a-t-il une documentation qui en parle?
  • Ou y a-t-il un moyen approprié de disposer de l' Taskobjet que j'ai manqué?
  • Ou existe-t-il une autre façon de faire feu et oublier les tâches avec le TPL?
Simon P Stevens
la source
1
En relation: Le bon moyen de feu et d'oublier (voir la réponse )
Simon P Stevens

Réponses:

108

Il y a une discussion à ce sujet dans les forums MSDN .

Stephen Toub, membre de l'équipe Microsoft pfx a ceci à dire:

Task.Dispose existe en raison du fait que Task encapsule potentiellement un handle d'événement utilisé lors de l'attente de la fin de la tâche, dans le cas où le thread en attente doit réellement bloquer (par opposition à la rotation ou potentiellement à l'exécution de la tâche en attente). Si tout ce que vous faites est d'utiliser des continuations, ce handle d'événement ne sera jamais alloué
...
il est probablement préférable de s'appuyer sur la finalisation pour s'occuper des choses.

Mise à jour (octobre 2012)
Stephen Toub a publié un blog intitulé Dois-je me débarrasser des tâches? qui donne plus de détails et explique les améliorations de .Net 4.5.

En résumé: vous n'avez pas besoin de vous débarrasser des Taskobjets 99% du temps.

Il y a deux raisons principales pour supprimer un objet: pour libérer des ressources non gérées de manière déterministe et opportune, et pour éviter le coût de l'exécution du finaliseur de l'objet. Aucun de ces éléments ne s'applique à la Taskplupart du temps:

  1. À partir de .Net 4.5, la seule fois où a Taskalloue le handle d'attente interne (la seule ressource non gérée dans l' Taskobjet) est lorsque vous utilisez explicitement le IAsyncResult.AsyncWaitHandlede Task, et
  2. L' Taskobjet lui-même n'a pas de finaliseur; le handle est lui-même enveloppé dans un objet avec un finaliseur, donc à moins qu'il ne soit alloué, il n'y a pas de finaliseur à exécuter.
Kirill Muzykov
la source
3
Merci, intéressant. Cela va cependant à l'encontre de la documentation MSDN. Y a-t-il un mot officiel de MS ou de l'équipe .net indiquant que ce code est acceptable? Il y a aussi le point soulevé à la fin de cette discussion que "et si l'implémentation change dans une version future"
Simon P Stevens
En fait, je viens de remarquer que le répondeur dans ce fil de discussion fonctionne effectivement chez Microsoft, apparemment dans l'équipe pfx, donc je suppose que c'est une sorte de réponse officielle. Mais il y a une suggestion vers le bas qui ne fonctionne pas dans tous les cas. S'il y a une fuite potentielle, est-ce que je ferais mieux de simplement revenir à ThreadPool.QueueUserWorkItem dont je sais qu'il est sûr?
Simon P Stevens
Oui, il est très étrange qu'il existe un Dispose que vous pourriez ne pas appeler. Si vous regardez des exemples ici msdn.microsoft.com/en-us/library/dd537610.aspx et ici msdn.microsoft.com/en-us/library/dd537609.aspx, ils ne suppriment pas les tâches. Cependant, les exemples de code dans MSDN présentent parfois de très mauvaises techniques. Le gars a également répondu à la question qui fonctionne pour Microsoft.
Kirill Muzykov
2
@Simon: (1) Le document MSDN que vous citez est le conseil générique, les cas spécifiques ont des conseils plus spécifiques (par exemple, ne pas avoir besoin d'utiliser EndInvokedans WinForms lors de l'utilisation BeginInvokepour exécuter du code sur le thread d'interface utilisateur). (2) Stephen Toub est assez bien connu en tant qu'orateur régulier sur l'utilisation efficace de PFX (par exemple sur channel9.msdn.com ), donc si quelqu'un peut donner de bons conseils, c'est bien. Notez son deuxième paragraphe: il y a des moments où laisser les choses au finaliser c'est mieux.
Richard
12

C'est le même type de problème qu'avec la classe Thread. Il consomme 5 descripteurs de système d'exploitation mais n'implémente pas IDisposable. Bonne décision des concepteurs d'origine, il existe bien sûr peu de moyens raisonnables d'appeler la méthode Dispose (). Vous devrez d'abord appeler Join ().

La classe Task ajoute un handle à cela, un événement de réinitialisation manuelle interne. Quelle est la ressource de système d'exploitation la moins chère qui soit. Bien sûr, sa méthode Dispose () ne peut libérer qu'un seul descripteur d'événement, pas les 5 descripteurs consommés par Thread. Ouais, ne t'en fais pas .

Attention, vous devriez être intéressé par la propriété IsFaulted de la tâche. C'est un sujet assez moche, vous pouvez en savoir plus à ce sujet dans cet article de MSDN Library . Une fois que vous avez géré cela correctement, vous devriez également avoir une bonne place dans votre code pour éliminer les tâches.

Hans Passant
la source
6
Mais une tâche ne crée pas Threaddans la plupart des cas, elle utilise le ThreadPool.
svick
-1

J'adorerais voir quelqu'un peser sur la technique présentée dans cet article: Invocation de délégué asynchrone Typeafe Fire-and-forget en C #

Il semble qu'une méthode d'extension simple gérera tous les cas triviaux d'interaction avec les tâches et pourra appeler disposer dessus.

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
la source
3
Bien sûr, cela ne parvient pas à éliminer l' Taskinstance renvoyée par ContinueWith, mais voir la citation de Stephen Toub est la réponse acceptée: il n'y a rien à éliminer si rien n'effectue une attente bloquante sur une tâche.
Richard
1
Comme le mentionne Richard, ContinueWith (...) renvoie également un deuxième objet Task qui n'est alors pas supprimé.
Simon P Stevens
1
Donc, dans l'état actuel des choses, le code ContinueWith est en fait pire que rédudant car il entraînera la création d'une autre tâche juste pour éliminer l'ancienne tâche. Dans l'état actuel des choses, il ne serait fondamentalement pas possible d'introduire une attente de blocage dans ce bloc de code, à part si le délégué d'action que vous lui avez transmis essayait de manipuler les tâches lui-même.
Chris Marisic
1
Vous pourrez peut-être utiliser la façon dont les lambdas capturent les variables de manière légèrement délicate pour vous occuper de la deuxième tâche. Task disper = null; disper = tsk.ContinueWith(cnt => { cnt.Dispose(); disper.Dispose(); });
Gideon Engelberth
@GideonEngelberth qui devrait apparemment fonctionner. Puisque disper ne devrait jamais être éliminé par le GC, il devrait rester valide jusqu'à ce que le lambda s'appelle lui-même pour se débarrasser, en supposant que la référence y est toujours valide / hausser les épaules. Peut-être a-t-il besoin d'un essai / attraper vide autour de lui?
Chris Marisic