Un guide du programmeur .NET pour CancellationToken

Publié: 2022-08-23

Parfois, 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
Résumé de l'approche d'annulation et exemples de langage

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:

  1. Créez une classe RandomWorker .
  2. RandomWorker doit avoir une méthode DoWorkAsync qui exécute un travail aléatoire.
  3. 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.

De haut en bas, les mots "Gold" (or coloré), "Microsoft" et "Partner" (tous deux en noir) apparaissent suivis du logo Microsoft.
En tant que Microsoft Gold Partner, Toptal est votre réseau d'élite d'experts Microsoft. Constituez des équipes performantes avec les experts dont vous avez besoin, où que vous soyez et exactement quand vous en avez besoin !

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