Ein .NET-Programmiererhandbuch zu CancellationToken

Veröffentlicht: 2022-08-23

Manchmal ist eine Kündigung eine gute Sache. In vielen meiner .NET-Projekte hatte ich reichlich Motivation, sowohl interne als auch externe Prozesse abzubrechen. Microsoft erfuhr, dass sich Entwickler diesem häufigen Anwendungsfall in einer Vielzahl komplexer Implementierungen näherten, und entschied, dass es einen besseren Weg geben muss. Daher wurde ein allgemeines Abbruchkommunikationsmuster als CancellationToken eingeführt, das unter Verwendung von Multithreading- und Interprozesskommunikationskonstrukten auf niedrigerer Ebene erstellt wurde. Als Teil meiner anfänglichen Recherche zu diesem Muster – und nachdem ich den eigentlichen .NET-Quellcode für die Microsoft-Implementierung durchforstet hatte – stellte ich fest, dass CancellationToken eine viel breitere Reihe von Problemen lösen kann: Abonnements für den Ausführungsstatus von Anwendungen, Zeitüberschreitung von Vorgängen mit unterschiedlichen Trigger und allgemeine Kommunikation zwischen Prozessen über Flags.

Der beabsichtigte CancellationToken-Anwendungsfall

CancellationToken wurde in .NET 4 eingeführt, um die vorhandenen Lösungen zum Abbrechen von Vorgängen zu verbessern und zu standardisieren. Es gibt vier allgemeine Ansätze zum Umgang mit Stornierungen, die beliebte Programmiersprachen in der Regel implementieren:

Töten Tell, nimm kein Nein als Antwort Fragen Sie höflich und akzeptieren Sie eine Absage Höflich Flagge setzen, abfragen lassen, wenn es will
Sich nähern Harter Stopp; Unstimmigkeiten später beheben Sagen Sie ihm, er soll aufhören, aber lassen Sie ihn die Dinge aufräumen Eine direkte, aber sanfte Aufforderung aufzuhören Bitten Sie es, damit aufzuhören, aber erzwingen Sie es nicht
Zusammenfassung Ein todsicherer Weg zu Korruption und Schmerz Ermöglicht saubere Stopppunkte, muss aber stoppen Ermöglicht saubere Haltepunkte, aber die Stornierungsanforderung kann ignoriert werden Die Stornierung wird über ein Flag angefordert
Pthreads pthread_kill ,
pthread_cancel (asynchron)
pthread_cancel (verzögerter Modus) n / A Durch eine Fahne
.NETZ Thread.Abort n / A Thread.Interrupt Durch ein Flag in CancellationToken
Java Thread.destroy ,
Thread.stop
n / A Thread.interrupt Durch ein Flag oder Thread.interrupted
Python PyThreadState_SetAsyncExc n / A asyncio.Task.cancel Durch eine Fahne
Orientierungshilfe Inakzeptabel; vermeiden Sie diese Vorgehensweise Akzeptabel, insbesondere wenn eine Sprache keine Ausnahmen oder Abwicklung unterstützt Akzeptabel, wenn die Sprache dies unterstützt Besser, aber eher eine Gruppenarbeit
Zusammenfassung des Stornierungsansatzes und Sprachbeispiele

CancellationToken befindet sich in der letzten Kategorie, in der die Stornierungskonversation kooperativ ist.

Nachdem Microsoft CancellationToken eingeführt hatte, wurde es von der Entwicklergemeinschaft schnell angenommen, insbesondere weil viele wichtige .NET-APIs aktualisiert wurden, um diese Token nativ zu verwenden. Beispielsweise unterstützen Aktionen ab ASP.NET Core 2.0 einen optionalen CancellationToken -Parameter, der signalisieren kann, ob eine HTTP-Anforderung geschlossen wurde, wodurch jeder Vorgang abgebrochen werden kann und somit eine unnötige Nutzung von Ressourcen vermieden wird.

Nach einem tiefen Einblick in die .NET-Codebasis wurde klar, dass die Verwendung von CancellationToken nicht auf die Stornierung beschränkt ist.

Stornierungstoken unter einem Mikroskop

Wenn wir uns die Implementierung von CancellationToken genauer ansehen, sehen wir, dass es sich nur um ein einfaches Flag (dh ManualResetEvent ) und die unterstützende Infrastruktur handelt, die die Möglichkeit bietet, dieses Flag zu überwachen und zu ändern. Das Hauptdienstprogramm von CancellationToken steckt in seinem Namen, was darauf hindeutet, dass dies die übliche Methode zum Abbrechen von Vorgängen ist. Heutzutage ermöglichen alle .NET-Bibliotheken, -Pakete oder -Frameworks mit asynchronen oder lang andauernden Vorgängen die Stornierung über diese Token.

CancellationToken kann ausgelöst werden, indem entweder sein Flag manuell auf „true“ gesetzt oder so programmiert wird, dass es nach Ablauf einer bestimmten Zeitspanne auf „true“ wechselt. Unabhängig davon, wie ein CancellationToken ausgelöst wird, kann der Clientcode, der dieses Token überwacht, den Wert des Token-Flags durch eine von drei Methoden bestimmen:

  • Verwenden eines WaitHandle
  • Abfragen des CancellationToken -Flags
  • Informieren des Clientcodes, wenn der Status des Flags durch ein programmgesteuertes Abonnement aktualisiert wird

Nach weiterer Recherche in der .NET-Codebasis stellte sich heraus, dass das .NET-Team CancellationTokens in anderen Szenarien, die nicht mit der Stornierung verbunden sind, nützlich fand. Sehen wir uns einige dieser fortgeschrittenen und Off-Brand-Anwendungsfälle an, die C#-Entwicklern Multithread- und Interprozess-Koordination ermöglichen, um komplexe Situationen zu vereinfachen.

CancellationTokens für erweiterte Ereignisse

Beim Schreiben von ASP.NET Core-Anwendungen müssen wir manchmal wissen, wann unsere Anwendung gestartet wurde, oder wir müssen unseren Code in den Prozess zum Herunterfahren des Hosts einfügen. In diesen Fällen verwenden wir die Schnittstelle IHostApplicationLifetime (früher IApplicationLifetime ). Diese Schnittstelle (aus dem Repository von .NET Core) verwendet CancellationToken , um drei Hauptereignisse zu kommunizieren: ApplicationStarted , ApplicationStopping und 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(); } }

Auf den ersten Blick scheint es so, als ob CancellationToken hier nicht hingehören, zumal sie als Events verwendet werden. Eine weitere Untersuchung zeigt jedoch, dass diese Token perfekt passen:

  • Sie sind flexibel und bieten dem Client der Schnittstelle mehrere Möglichkeiten, diese Ereignisse abzuhören.
  • Sie sind Thread-sicher aus der Box.
  • Sie können aus verschiedenen Quellen erstellt werden, indem CancellationToken s kombiniert werden.

Obwohl CancellationToken nicht für alle Ereignisanforderungen perfekt geeignet sind, eignen sie sich ideal für Ereignisse, die nur einmal auftreten, wie z. B. das Starten oder Beenden einer Anwendung.

CancellationToken für Timeout

ASP.NET gibt uns standardmäßig sehr wenig Zeit zum Herunterfahren. In den Fällen, in denen wir etwas mehr Zeit benötigen, können wir mit der integrierten HostOptions -Klasse diesen Timeout-Wert ändern. Darunter wird dieser Timeout-Wert in ein CancellationToken verpackt und in die zugrunde liegenden Unterprozesse eingespeist.

Die Methode IHostedService von StopAsync ist ein hervorragendes Beispiel für diese Verwendung:

 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); } }

Wie aus der IHostedService Schnittstellendefinition hervorgeht, akzeptiert die StopAsync Methode einen CancellationToken -Parameter. Der Kommentar, der diesem Parameter zugeordnet ist, macht deutlich, dass Microsofts ursprüngliche Absicht für CancellationToken eher ein Zeitüberschreitungsmechanismus als ein Abbruchprozess war.

Wenn diese Schnittstelle vor CancellationToken existiert hätte, hätte dies meiner Meinung nach ein TimeSpan -Parameter sein können, um anzugeben, wie lange die Stoppoperation verarbeitet werden durfte. Timeout-Szenarien lassen sich meiner Erfahrung nach fast immer in ein CancellationToken mit großem Zusatznutzen umwandeln.

Vergessen wir für den Moment, dass wir wissen, wie die StopAsync Methode konzipiert ist, und denken Sie stattdessen darüber nach, wie wir den Vertrag dieser Methode entwerfen würden. Lassen Sie uns zunächst die Anforderungen definieren:

  • Die StopAsync Methode muss versuchen, den Dienst zu beenden.
  • Die StopAsync Methode sollte einen ordnungsgemäßen Stoppzustand aufweisen.
  • Unabhängig davon, ob ein ordnungsgemäßer Stoppzustand erreicht wird, muss ein gehosteter Dienst eine maximale Zeit zum Stoppen haben, wie durch unseren Timeout-Parameter definiert.

Indem wir eine StopAsync Methode in irgendeiner Form haben, erfüllen wir die erste Anforderung. Die restlichen Anforderungen sind knifflig. CancellationToken erfüllt genau diese Anforderungen, indem es ein standardmäßiges .NET-Flag-basiertes Kommunikationstool verwendet, um die Konversation zu stärken.

CancellationToken als Benachrichtigungsmechanismus

Das größte Geheimnis hinter CancellationToken ist, dass es nur ein Flag ist. Lassen Sie uns veranschaulichen, wie CancellationToken verwendet werden kann, um Prozesse zu starten, anstatt sie zu stoppen.

Folgendes berücksichtigen:

  1. Erstellen Sie eine RandomWorker -Klasse.
  2. RandomWorker sollte über eine DoWorkAsync Methode verfügen, die zufällige Arbeiten ausführt.
  3. Die DoWorkAsync Methode muss es einem Aufrufer ermöglichen, anzugeben, wann die Arbeit beginnen soll.
 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); } } }

Die obige Klasse erfüllt die ersten beiden Anforderungen und lässt uns mit der dritten zurück. Es gibt mehrere alternative Schnittstellen, die wir verwenden könnten, um unseren Worker auszulösen, wie eine Zeitspanne oder ein einfaches Flag:

 # With a time span Task DoWorkAsync(TimeSpan startAfter); # Or a simple flag bool ShouldStart { get; set; } Task DoWorkAsync();

Diese beiden Ansätze sind in Ordnung, aber nichts ist so elegant wie die Verwendung eines 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); } } }

Dieser Beispiel-Client-Code veranschaulicht die Leistungsfähigkeit dieses Designs:

 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); } } }

Die CancellationTokenSource erstellt unser CancellationToken hinter den Kulissen und koordiniert das Auslösen aller zugehörigen Prozesse. In diesem Fall ist der zugehörige Prozess unser RandomWorker , der darauf wartet, gestartet zu werden. Dieser Ansatz ermöglicht es uns, die Thread-Sicherheit zu nutzen, die in die standardmäßige CancellationToken Implementierung integriert ist.

Eine umfangreiche CancellationToken-Toolbox

Diese Beispiele zeigen, wie CancellationToken eine Toolbox mit Lösungen bereitstellt, die außerhalb des beabsichtigten Anwendungsfalls nützlich sind. Die Tools können sich in vielen Szenarien als nützlich erweisen, die Flag-basierte Kommunikation zwischen Prozessen beinhalten. Egal, ob wir mit Zeitüberschreitungen, Benachrichtigungen oder einmaligen Ereignissen konfrontiert sind, wir können auf diese elegante, von Microsoft getestete Implementierung zurückgreifen.

Von oben nach unten erscheinen die Wörter „Gold“ (goldfarben), „Microsoft“ und „Partner“ (beide in Schwarz), gefolgt vom Microsoft-Logo.
Als Microsoft Gold Partner ist Toptal Ihr Elite-Netzwerk von Microsoft-Experten. Bauen Sie leistungsstarke Teams mit den Experten auf, die Sie brauchen – überall und genau dann, wenn Sie sie brauchen!

Weiterführende Literatur im Toptal Engineering Blog:

  • .NET Core: Going Wild und Open Source. Microsoft, warum hast du so lange gebraucht?!
  • Erstellen einer ASP.NET-Web-API mit ASP.NET Core
  • Anleitung zum Booten und Erstellen von .NET-Projekten