Pourquoi CancellationToken est-il distinct de CancellationTokenSource?

137

Je cherche une raison pour laquelle .NET CancellationTokenstruct a été introduit en plus de la CancellationTokenSourceclasse. Je comprends comment l'API doit être utilisée, mais je veux également comprendre pourquoi elle est conçue de cette façon.

Ie, pourquoi avons-nous:

var cts = new CancellationTokenSource();
SomeCancellableOperation(cts.Token);

...
public void SomeCancellableOperation(CancellationToken token) {
    ...
    token.ThrowIfCancellationRequested();
    ...
}

au lieu de passer directement CancellationTokenSourcecomme:

var cts = new CancellationTokenSource();
SomeCancellableOperation(cts);

...
public void SomeCancellableOperation(CancellationTokenSource cts) {
    ...
    cts.ThrowIfCancellationRequested();
    ...
}

S'agit-il d'une optimisation des performances basée sur le fait que les vérifications d'état d'annulation se produisent plus fréquemment que la transmission du jeton?

Pour que cela CancellationTokenSourcepuisse suivre et mettre à jour CancellationTokens, et pour chaque jeton, le contrôle d'annulation est un accès au champ local?

Étant donné qu'un bool volatile sans verrouillage est suffisant dans les deux cas, je ne vois toujours pas pourquoi ce serait plus rapide.

Merci!

Andrey Tarantsov
la source

Réponses:

110

J'ai participé à la conception et à l'implémentation de ces classes.

La réponse courte est « séparation des préoccupations ». Il est bien vrai qu'il existe différentes stratégies de mise en œuvre et que certaines sont plus simples au moins en ce qui concerne le système de types et l'apprentissage initial. Cependant, CTS et CT sont destinés à être utilisés dans de nombreux scénarios (tels que les piles de bibliothèques profondes, le calcul parallèle, l'asynchrone, etc.) et ont donc été conçus avec de nombreux cas d'utilisation complexes à l'esprit. C'est une conception destinée à encourager les modèles réussis et à décourager les anti-modèles sans sacrifier les performances.

Si la porte était laissée ouverte à des API qui se comportaient mal, l'utilité de la conception d'annulation pourrait rapidement être érodée.

CancellationTokenSource == "déclenchement d'annulation", plus génère des écouteurs liés

CancellationToken == "auditeur d'annulation"

Mike Liddell
la source
7
Merci beaucoup! Les connaissances internes sont vraiment appréciées.
Andrey Tarantsov
stackoverflow.com/questions/39077497/... Avez-vous une idée du délai d'expiration par défaut pour le flux d'entrée du StreamSocket? Lorsque j'utilise le jeton d'annulation pour annuler l'opération de lecture, cela ferme également le socket concerné. Peut-il y avoir un moyen de résoudre ce problème?
sam18
@Mike - Juste curieux: comment se fait-il que vous ne puissiez pas appeler quelque chose comme ThrowIfCancellationRequested () sur un CTS comme vous le pouvez sur un CT?
rory.ap
Ils ont des responsabilités différentes et il n'est pas trop verbeux d'écrire cts.Token.ThrowIfCancellationRequested () donc nous n'avons pas ajouté l'API d'écoute directement sur CTS.
Mike Liddell
@Mike Pourquoi annulationtoken est une structure et non une classe? Je ne vois aucun avantage en
termes de
85

J'avais la question exacte et je voulais comprendre la raison d'être de cette conception.

La réponse acceptée a parfaitement justifié la justification. Voici la confirmation de l'équipe qui a conçu cette fonctionnalité (c'est moi qui souligne):

Deux nouveaux types forment la base du cadre: A CancellationTokenest une structure qui représente une «demande potentielle d'annulation». Cette structure est passée dans les appels de méthode en tant que paramètre et la méthode peut l'interroger ou enregistrer un rappel à déclencher lorsque l'annulation est demandée. A CancellationTokenSourceest une classe qui fournit le mécanisme pour lancer une demande d'annulation et il a une Token propriété pour obtenir un jeton associé. Il aurait été naturel de combiner ces deux classes en une seule, mais cette conception permet aux deux opérations clés (lancement d'une demande d'annulation vs observation et réponse à une annulation) d'être clairement séparées. En particulier, les méthodes qui ne prennent qu'un CancellationTokenpeut observer une demande d'annulation mais ne peuvent pas en lancer une.

Lien: Framework d'annulation .NET 4

À mon avis, le fait que l' CancellationTokenon ne puisse qu'observer l'état et ne pas le changer est extrêmement critique. Vous pouvez distribuer le jeton comme un bonbon et ne jamais craindre que quelqu'un d'autre, autre que vous, l'annule. Il vous protège du code tiers hostile. Oui, les chances sont minces, mais j'aime personnellement cette garantie.

Je pense également que cela rend l'API plus propre et évite les erreurs accidentelles et favorise une meilleure conception des composants.

Regardons l'API publique pour ces deux classes.

API CancellationToken

API CancellationTokenSource

Si vous deviez les combiner, lors de l'écriture de LongRunningFunction, je verrai des méthodes comme ces multiples surcharges de 'Cancel' que je ne devrais pas utiliser. Personnellement, je déteste aussi voir la méthode Dispose.

Je pense que la conception actuelle de la classe suit la philosophie du «gouffre du succès», elle guide les développeurs pour créer de meilleurs composants capables de gérer l' Taskannulation, puis les instrumenter ensemble de nombreuses manières pour créer des flux de travail compliqués.

Permettez-moi de vous poser une question, vous êtes-vous demandé à quoi sert le jeton. Cela n'avait aucun sens pour moi. Et puis j'ai lu Annulation dans Managed Threads et tout est devenu limpide.

Je crois que la conception du cadre d'annulation dans TPL est absolument de premier ordre.

SolutionYogi
la source
2
Si vous vous demandez comment le CancellationTokenSourcepeut réellement lancer la demande d'annulation sur son jeton associé (le jeton ne peut pas le faire par lui-même): Le CancellationToken a ce constructeur interne: internal CancellationToken(CancellationTokenSource source) { this.m_source = source; }et cette propriété: public bool IsCancellationRequested { get { return this.m_source != null && this.m_source.IsCancellationRequested; } }Le CancellationTokenSource utilise le constructeur interne, donc le jeton fait référence au source (m_source)
chviLadislav
65

Ils ne sont pas séparés pour des raisons techniques mais sémantiques. Si vous regardez l'implémentation de CancellationTokenunder ILSpy, vous constaterez que c'est simplement un wrapper CancellationTokenSource(et donc pas différent en termes de performances que de passer une référence).

Ils fournissent cette séparation des fonctionnalités pour rendre les choses plus prévisibles: lorsque vous passez une méthode a CancellationToken, vous savez que vous êtes toujours le seul à pouvoir l'annuler. Bien sûr, la méthode pourrait toujours lancer un TaskCancelledException, mais CancellationTokenelle-même - et toute autre méthode référençant le même jeton - resterait sûre.

Cory Nelson
la source
Merci! Ma réaction clignotante est que, sémantiquement, c'est une approche discutable, à égalité avec la fourniture d'IReadOnlyList. Cela semble très plausible, cependant, acceptez votre réponse.
Andrey Tarantsov
1
Après réflexion, je me retrouve à remettre en question la plausibilité. Ne serait-il pas plus logique, alors, de fournir une interface ICancellationToken que CancellationTokenSource implémenterait? Je souhaite que quelqu'un de l'équipe .NET
intervienne
Cela pourrait encore amener un programmeur astucieux à lancer un casting CancellationTokenSource. On pourrait penser que vous pourriez simplement dire "ne faites pas ça", mais les gens (y compris moi!) Font parfois ces choses de toute façon pour accéder à une fonctionnalité cachée, et cela arriverait. C'est du moins ma théorie actuelle.
Cory Nelson
1
Ce n'est pas ainsi que vous concevez des API dans la plupart des cas, à moins que ce ne soit lié à la sécurité. Je préfère parier sur une autre explication, comme «garder leurs options ouvertes pour des optimisations de performances à l'avenir». Mais encore, le vôtre est le meilleur à ce jour.
Andrey Tarantsov
Hé, j'espère que vous m'excuserez de réattribuer la réponse acceptée à la personne impliquée dans la mise en œuvre. Bien que vous disiez tous les deux la même chose, je pense que SO bénéficie du fait que la réponse définitive est au top.
Andrey Tarantsov
10

Le CancellationTokenest une structure tellement de copies pourraient exister en raison de sa transmission aux méthodes.

Les CancellationTokenSourcedéfinit l'état de toutes les copies d'un jeton lors de l' appel Cancelà la source. Voir cette page MSDN

La raison de la conception pourrait être juste une question de séparation des préoccupations et de la vitesse d'une structure.

Euh non
la source
1
Merci. Notez qu'une autre réponse indique que techniquement (en IL), CancellationTokenSource ne définit pas l'état des jetons; au lieu de cela, les jetons enveloppent une référence réelle à CancellationTokenSource et y accèdent simplement pour vérifier l'annulation. Au contraire, la vitesse ne peut être perdue qu'ici.
Andrey Tarantsov
+1. Je ne comprends pas. s'il s'agit d'un type valeur, chaque méthode a sa propre valeur (valeur distincte). alors comment une méthode sait-elle si une annulation a été appelée? il n'a AUCUNE référence à TokenSource. tout ce que la méthode peut voir est un type de valeur locale comme "5". peux-tu expliquer ?
Royi Namir
1
@RoyiNamir: Chaque CancellationToken a une référence privée "m_source" de type CancellationTokenSource
Andreyul
2

C'est CancellationTokenSourcela «chose» qui émet l'annulation, quelle qu'en soit la raison. Il a besoin d 'un moyen d' «envoyer» cette annulation à tous les CancellationTokenmessages émis. C'est ainsi que, par exemple, ASP.NET peut annuler des opérations lorsqu'une demande est abandonnée. Chaque demande a un CancellationTokenSourcequi transmet l'annulation à tous les jetons qu'elle a émis.

C'est idéal pour les tests unitaires BTW - créez votre propre source de jeton d'annulation, obtenez un jeton, appelez Cancella source et transmettez le jeton à votre code qui doit gérer l'annulation.

n8wrl
la source
Merci - mais les deux responsabilités (qui sont très liées) pourraient être confiées à la même classe. N'explique pas vraiment pourquoi ils sont séparés.
Andrey Tarantsov