web-dev-qa-db-fra.com

Quand disposer d'AnnulationTokenSource?

La classe CancellationTokenSource est jetable. Un rapide coup d’œil dans Reflector prouve l’utilisation de KernelEvent, une ressource (très probablement) non gérée. Puisque CancellationTokenSource ne possède pas de finaliseur, si nous ne le supprimons pas, le GC ne le fera pas.

En revanche, si vous consultez les exemples répertoriés dans l'article MSDN Annulation dans les threads gérés , un seul extrait de code dispose du jeton.

Quelle est la bonne façon de l'éliminer dans le code?

  1. Vous ne pouvez pas encapsuler le code commençant votre tâche parallèle avec using si vous ne l'attendez pas. Et il est logique d’annuler uniquement si vous n’attendez pas.
  2. Bien sûr, vous pouvez ajouter ContinueWith à la tâche avec un appel Dispose, mais est-ce la solution?
  3. Qu'en est-il des requêtes PLINQ annulables, qui ne se synchronisent pas, mais font simplement quelque chose à la fin? Disons que .ForAll(x => Console.Write(x))?
  4. Est-ce réutilisable? Peut-on utiliser le même jeton pour plusieurs appels, puis le disposer avec le composant Host, par exemple le contrôle de l'interface utilisateur?

Comme il n’a pas quelque chose de semblable à une méthode Reset pour nettoyer les champs IsCancelRequested et Token, je suppose qu’il n’est pas réutilisable. Ainsi, chaque fois que vous démarrez une tâche (ou PLINQ) vous devez en créer une nouvelle. Est-ce vrai? Si oui, ma question est quelle est la stratégie correcte et recommandée pour traiter Dispose sur ces nombreuses instances CancellationTokenSource?

139
George Mamaladze

Pour savoir s'il est vraiment nécessaire d'appeler Dispose sur CancellationTokenSource... j'ai eu une fuite de mémoire dans mon projet et il s'est avéré que CancellationTokenSource était le problème.

Mon projet propose un service qui lit constamment la base de données et exécute différentes tâches. Je transmettais des jetons d'annulation liés à mes employés. Ainsi, même après la fin du traitement des données, les jetons d'annulation n'étaient pas supprimés, ce qui provoquait une fuite de mémoire.

MSDN Annulation dans les threads gérés indique clairement:

Notez que vous devez appeler Dispose sur la source du jeton lié lorsque vous avez terminé. Pour un exemple plus complet, voir Comment: écouter plusieurs demandes d'annulation .

J'ai utilisé ContinueWith dans mon implémentation.

64
Gruzilkin

Je ne pensais pas que les réponses actuelles étaient satisfaisantes. Après des recherches, j'ai trouvé cette réponse de Stephen Toub ( référence ):

Ça dépend. Dans .NET 4, CTS.Dispose servait à deux fins principales. Si WaitHandle de CancellationToken avait été utilisé (en l'attribuant par conséquent), Dispose en disposera. De plus, si le CTS a été créé via la méthode CreateLinkedTokenSource, Dispose dissociera le CTS des jetons auxquels il était lié. Dans .NET 4.5, Dispose a un objectif supplémentaire: si le CTS utilise une minuterie sous les capots (par exemple, CancelAfter a été appelé), la minuterie sera mise au rebut.

Il est très rare que CancellationToken.WaitHandle soit utilisé, aussi, nettoyer après cela n'est généralement pas une bonne raison d'utiliser Dispose. Si, toutefois, vous créez votre CTS avec CreateLinkedTokenSource ou si vous utilisez la fonctionnalité de minuterie de CTS, l'utilisation de Dispose peut avoir plus d'impact.

Je pense que la partie audacieuse est la partie importante. Il utilise "plus percutant" ce qui le rend un peu vague. J'interprète cela comme voulant dire qu'il faut appeler Dispose dans ces situations-là, sinon utiliser Dispose n'est pas nécessaire.

35
Jesse Good

J'ai jeté un œil dans ILSpy pour le CancellationTokenSource, mais je ne trouve que m_KernelEvent qui est en fait un ManualResetEvent, qui est une classe wrapper pour un objet WaitHandle. Cela devrait être géré correctement par le GC.

25
Bryan Crosby

Vous devriez toujours disposer CancellationTokenSource.

Comment en disposer dépend exactement du scénario. Vous proposez plusieurs scénarios différents.

  1. using ne fonctionne que lorsque vous utilisez CancellationTokenSource sur un travail parallèle que vous attendez. Si c'est votre senario, alors génial, c'est la méthode la plus simple.

  2. Lorsque vous utilisez des tâches, utilisez la tâche ContinueWith que vous avez indiquée pour éliminer CancellationTokenSource.

  3. Pour plinq, vous pouvez utiliser using puisque vous l'exécutez en parallèle mais attendez que tous les travailleurs exécutant parallèlement soient terminés.

  4. Pour l'interface utilisateur, vous pouvez créer un nouveau CancellationTokenSource pour chaque opération pouvant être annulée qui n'est pas liée à un seul déclencheur d'annulation. Maintenir un List<IDisposable> et ajoutez chaque source à la liste, en les supprimant lorsque votre composant est supprimé.

  5. Pour les threads, créez un nouveau thread qui joint tous les threads de travail et ferme la source unique lorsque tous les threads de travail sont terminés. Voir CancellationTokenSource, Quand en disposer?

Il y a toujours un moyen. IDisposable instances doivent toujours être supprimées. Les échantillons ne le font pas souvent parce qu'ils sont soit des échantillons rapides pour montrer l'utilisation du coeur, soit parce que l'ajout de tous les aspects de la classe démontrée serait trop complexe pour un échantillon. L’échantillon n’est qu’un échantillon, pas nécessairement (ni même habituellement) un code de qualité de la production. Tous les échantillons ne peuvent pas être copiés tels quels dans le code de production.

20
Samuel Neff

Cette réponse est encore à venir dans les recherches Google, et je crois que la réponse qui a été votée est qu'elle ne donne pas toute l'histoire. Après avoir examiné code source pour CancellationTokenSource (CTS) et CancellationToken (CT), je pense que dans la plupart des cas, la séquence de code suivante convient:

if (cancelTokenSource != null)
{
    cancelTokenSource.Cancel();
    cancelTokenSource.Dispose();
    cancelTokenSource = null;
}

Le m_kernelHandle _ Le champ interne mentionné ci-dessus est l’objet de synchronisation sauvegardant la propriété WaitHandle dans les classes CTS et CT. Il n'est instancié que si vous accédez à cette propriété. Donc, à moins que vous utilisiez WaitHandle pour une synchronisation de threads old-school dans votre appel Task, la disposition n'aura aucun effet.

Bien sûr, si vous l'utilisez , vous devriez faire ce que suggèrent les réponses précédentes et retarder l'appel de Dispose jusqu'à ce que WaitHandle les opérations utilisant le handle sont terminées, car, comme décrit dans la documentation de l'API Windows pour WaitHandle , les résultats ne sont pas définis.

14
jlyonsmith

Cela fait longtemps que je n'ai pas posé cette question et obtenu de nombreuses réponses utiles, mais je suis tombé sur un problème intéressant et j'ai pensé le poster ici comme une autre réponse:

Vous ne devez appeler CancellationTokenSource.Dispose() que lorsque vous êtes certain que personne ne tentera d'obtenir la propriété Token de la CTS. Sinon, vous devriez ne pas l'appeler , car c'est une course. Par exemple, voir ici:

https://github.com/aspnet/AspNetKatana/issues/108

Dans le correctif de ce problème, le code précédemment utilisé par cts.Cancel(); cts.Dispose(); a été modifié pour ne faire que cts.Cancel(); car quiconque ayant la malchance d'essayer d'obtenir le jeton d'annulation afin de respecter son état d'annulation après Dispose a été appelé, mais devra aussi gérer ObjectDisposedException - en plus du OperationCanceledException pour lequel il était prévu. .

Tratcher fait une autre observation clé liée à ce correctif: "L'élimination n'est requise que pour les jetons qui ne seront pas annulés, car l'annulation effectue le même nettoyage." c'est-à-dire qu'il suffit de faire Cancel() au lieu de disposer, c'est vraiment bien!

3
Tim Lovell-Smith