Un guide du programmeur .NET pour CancellationToken
Publié: 2022-08-23Parfois, annuler est une bonne chose. Dans bon nombre de mes projets .NET, j'ai eu beaucoup de motivation pour annuler les processus internes et externes. Microsoft a appris que les développeurs abordaient ce cas d'utilisation courant dans une variété d'implémentations complexes et a décidé qu'il devait y avoir un meilleur moyen. Ainsi, un modèle de communication d'annulation commun a été introduit en tant que CancellationToken
, qui a été construit à l'aide de constructions de communication multithreading et interprocessus de niveau inférieur. Dans le cadre de mes recherches initiales sur ce modèle - et après avoir fouillé dans le code source .NET réel pour l'implémentation de Microsoft - j'ai découvert que CancellationToken
peut résoudre un ensemble de problèmes beaucoup plus large : abonnements aux états d'exécution des applications, temporisation des opérations utilisant différents déclencheurs et communications interprocessus générales via des drapeaux.
Le cas d'utilisation prévu du jeton d'annulation
CancellationToken
a été introduit dans .NET 4 comme moyen d'améliorer et de standardiser les solutions existantes pour l'annulation des opérations. Il existe quatre approches générales pour gérer l'annulation que les langages de programmation populaires ont tendance à implémenter :
Tuer | Dites, ne prenez pas non pour une réponse | Demandez poliment et acceptez le rejet | Définissez le drapeau poliment, laissez-le interroger s'il le souhaite | |
---|---|---|---|---|
Approcher | Arrêt brutal ; résoudre les incohérences plus tard | Dites-lui d'arrêter mais laissez-le nettoyer les choses | Une demande directe mais douce d'arrêter | Demandez-lui d'arrêter, mais ne le forcez pas |
Sommaire | Un chemin infaillible vers la corruption et la douleur | Permet des points d'arrêt propres mais il doit s'arrêter | Autorise les points d'arrêt propres, mais la demande d'annulation peut être ignorée | L'annulation est demandée par un drapeau |
Pthreads | pthread_kill ,pthread_cancel (asynchrone) | pthread_cancel (mode différé) | n / A | A travers un drapeau |
.RAPPORTER | Thread.Abort | n / A | Thread.Interrupt | Via un drapeau dans CancellationToken |
Java | Thread.destroy ,Thread.stop | n / A | Thread.interrupt | Via un drapeau ou Thread.interrupted |
Python | PyThreadState_SetAsyncExc | n / A | asyncio.Task.cancel | A travers un drapeau |
Conseils | Inacceptable; éviter cette approche | Acceptable, en particulier lorsqu'une langue ne prend pas en charge les exceptions ou le déroulement | Acceptable si la langue le prend en charge | Mieux, mais plus un effort de groupe |
CancellationToken
réside dans la dernière catégorie, où la conversation d'annulation est coopérative.
Après que Microsoft a introduit CancellationToken
, la communauté des développeurs l'a rapidement adopté, notamment parce que de nombreuses API .NET majeures ont été mises à jour pour utiliser ces jetons de manière native. Par exemple, à partir d'ASP.NET Core 2.0, les actions prennent en charge un paramètre CancellationToken
facultatif qui peut signaler si une requête HTTP a été fermée, permettant l'annulation de toute opération et évitant ainsi l'utilisation inutile des ressources.
Après une plongée approfondie dans la base de code .NET, il est devenu clair que l'utilisation de CancellationToken
ne se limite pas à l'annulation.
CancellationToken sous un microscope
En examinant de plus près l'implémentation de CancellationToken
, nous constatons qu'il ne s'agit que d'un simple indicateur (c'est-à-dire ManualResetEvent
) et de l'infrastructure de support qui permet de surveiller et de modifier cet indicateur. L'utilitaire principal de CancellationToken
se trouve dans son nom, ce qui suggère qu'il s'agit du moyen courant d'annuler des opérations. De nos jours, toute bibliothèque, package ou framework .NET avec des opérations asynchrones ou de longue durée permet l'annulation via ces jetons.
CancellationToken
peut être déclenché soit en définissant manuellement son indicateur sur "true" ou en le programmant pour qu'il passe à "true" après qu'un certain laps de temps s'est écoulé. Quelle que soit la manière dont un CancellationToken
est déclenché, le code client qui surveille ce jeton peut déterminer la valeur de l'indicateur de jeton via l'une des trois méthodes suivantes :
- Utilisation d'un
WaitHandle
- Interroger le drapeau de
CancellationToken
- Informer le code client lorsque l'état de l'indicateur est mis à jour via un abonnement programmatique
Après des recherches plus approfondies dans la base de code .NET, il est devenu évident que l'équipe .NET a trouvé CancellationTokens
utile dans d'autres scénarios non liés à l'annulation. Explorons quelques-uns de ces cas d'utilisation avancés et hors marque, qui permettent aux développeurs C# d'avoir une coordination multithread et interprocessus pour simplifier les situations complexes.
CancellationTokens pour les événements avancés
Lors de l'écriture d'applications ASP.NET Core, nous avons parfois besoin de savoir quand notre application a démarré, ou nous devons injecter notre code dans le processus d'arrêt de l'hôte. Dans ces cas, nous utilisons l'interface IHostApplicationLifetime
(anciennement IApplicationLifetime
). Cette interface (du référentiel de .NET Core) utilise CancellationToken
pour communiquer trois événements majeurs : ApplicationStarted
, ApplicationStopping
et ApplicationStopped
:
namespace Microsoft.Extensions.Hosting { /// <summary> /// Allows consumers to be notified of application lifetime events. /// This interface is not intended to be user-replaceable. /// </summary> public interface IHostApplicationLifetime { /// <summary> /// Triggered when the application host has fully started. /// </summary> CancellationToken ApplicationStarted { get; } /// <summary> /// Triggered when the application host is starting a graceful shutdown. /// Shutdown will block until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopping { get; } /// <summary> /// Triggered when the application host has completed a graceful shutdown. /// The application will not exit until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopped { get; } /// <summary> /// Requests termination of the current application. /// </summary> void StopApplication(); } }
À première vue, il peut sembler que les CancellationToken
n'ont pas leur place ici, d'autant plus qu'ils sont utilisés comme événements. Cependant, un examen plus approfondi révèle que ces jetons conviennent parfaitement :

- Ils sont flexibles, permettant au client de l'interface de multiples façons d'écouter ces événements.
- Ils sont thread-safe hors de la boîte.
- Ils peuvent être créés à partir de différentes sources en combinant
CancellationToken
s.
Bien que les CancellationToken
ne soient pas parfaits pour chaque besoin d'événement, ils sont idéaux pour les événements qui ne se produisent qu'une seule fois, comme le démarrage ou l'arrêt d'une application.
CancellationToken for Timeout
Par défaut, ASP.NET nous laisse très peu de temps pour nous arrêter. Dans les cas où nous voulons un peu plus de temps, l'utilisation de la classe HostOptions
nous permet de modifier cette valeur de délai d'attente. En dessous, cette valeur de délai d'attente est enveloppée dans un CancellationToken
et introduite dans les sous-processus sous-jacents.
La méthode IHostedService
de StopAsync
est un excellent exemple de cette utilisation :
namespace Microsoft.Extensions.Hosting { /// <summary> /// Defines methods for objects that are managed by the host. /// </summary> public interface IHostedService { /// <summary> /// Triggered when the application host is ready to start the service. /// </summary> /// <param name="cancellationToken">Indicates that the start /// process has been aborted.</param> Task StartAsync(CancellationToken cancellationToken); /// <summary> /// Triggered when the application host is performing a graceful shutdown. /// </summary> /// <param name="cancellationToken">Indicates that the shutdown /// process should no longer be graceful.</param> Task StopAsync(CancellationToken cancellationToken); } }
Comme indiqué dans la définition de l'interface IHostedService
, la méthode StopAsync
prend un paramètre CancellationToken
. Le commentaire associé à ce paramètre indique clairement que l'intention initiale de Microsoft pour CancellationToken
était un mécanisme de temporisation plutôt qu'un processus d'annulation.
À mon avis, si cette interface avait existé avant l'existence de CancellationToken
, cela aurait pu être un paramètre TimeSpan
- pour indiquer combien de temps l'opération d'arrêt était autorisée à traiter. D'après mon expérience, les scénarios de délai d'attente peuvent presque toujours être convertis en un CancellationToken
avec une grande utilité supplémentaire.
Pour le moment, oublions que nous savons comment la méthode StopAsync
est conçue et réfléchissons plutôt à la manière dont nous concevrons le contrat de cette méthode. Définissons d'abord les besoins :
- La méthode
StopAsync
doit essayer d'arrêter le service. - La méthode
StopAsync
doit avoir un état d'arrêt normal. - Indépendamment du fait qu'un état d'arrêt gracieux soit atteint ou non, un service hébergé doit disposer d'un délai maximal pour s'arrêter, tel que défini par notre paramètre de délai d'attente.
En ayant une méthode StopAsync
sous n'importe quelle forme, nous satisfaisons à la première exigence. Les exigences restantes sont délicates. CancellationToken
répond exactement à ces exigences en utilisant un outil de communication standard basé sur des indicateurs .NET pour renforcer la conversation.
CancellationToken en tant que mécanisme de notification
Le plus grand secret derrière CancellationToken
est qu'il ne s'agit que d'un drapeau. Illustrons comment CancellationToken
peut être utilisé pour démarrer des processus au lieu de les arrêter.
Considérer ce qui suit:
- Créez une classe
RandomWorker
. -
RandomWorker
doit avoir une méthodeDoWorkAsync
qui exécute un travail aléatoire. - La méthode
DoWorkAsync
doit permettre à un appelant de spécifier quand le travail doit commencer.
public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync() { for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }
La classe ci-dessus satisfait aux deux premières exigences, nous laissant avec la troisième. Il existe plusieurs interfaces alternatives que nous pourrions utiliser pour déclencher notre travailleur, comme un intervalle de temps ou un simple indicateur :
# With a time span Task DoWorkAsync(TimeSpan startAfter); # Or a simple flag bool ShouldStart { get; set; } Task DoWorkAsync();
Ces deux approches sont bonnes, mais rien n'est aussi élégant que d'utiliser un CancellationToken
:
public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync(CancellationToken startToken) { startToken.WaitHandle.WaitOne(); for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }
Cet exemple de code client illustre la puissance de cette conception :
using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CancelToStart { public class Program { static void Main(string[] args) { CancellationTokenSource startCts = new CancellationTokenSource(); startCts.CancelAfter(TimeSpan.FromSeconds(10)); var tasks = Enumerable.Range(0, 10) .Select(i => new RandomWorker(i)) .Select(worker => worker.DoWorkAsync(startCts.Token)) .ToArray(); Task.WaitAll(tasks, CancellationToken.None); } } }
Le CancellationTokenSource
créera notre CancellationToken
dans les coulisses et coordonnera le déclenchement de tous les processus associés. Dans ce cas, le processus associé est notre RandomWorker
, qui attend de démarrer. Cette approche nous permet de tirer parti de la sécurité des threads intégrée à l'implémentation par défaut de CancellationToken
.
Une vaste boîte à outils CancellationToken
Ces exemples montrent comment CancellationToken
fournit une boîte à outils de solutions utiles en dehors de son cas d'utilisation prévu. Les outils peuvent être utiles dans de nombreux scénarios impliquant une communication interprocessus basée sur des indicateurs. Que nous soyons confrontés à des délais d'attente, des notifications ou des événements ponctuels, nous pouvons nous rabattre sur cette élégante implémentation testée par Microsoft.
Lectures complémentaires sur le blog Toptal Engineering :
- .NET Core : Going Wild et Open Source. Microsoft, qu'est-ce qui vous a pris si longtemps ? !
- Construire une API Web ASP.NET avec ASP.NET Core
- Comment démarrer et créer des projets .NET