Ein .NET-Programmiererhandbuch zu CancellationToken
Veröffentlicht: 2022-08-23Manchmal 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 |
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:
- Erstellen Sie eine
RandomWorker
-Klasse. -
RandomWorker
sollte über eineDoWorkAsync
Methode verfügen, die zufällige Arbeiten ausführt. - 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.
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