Przewodnik programisty .NET dotyczący CancellationToken

Opublikowany: 2022-08-23

Czasami anulowanie jest dobrą rzeczą. W wielu moich projektach .NET miałem dużą motywację do anulowania zarówno wewnętrznych, jak i zewnętrznych procesów. Microsoft dowiedział się, że programiści podchodzą do tego powszechnego przypadku użycia w różnych złożonych implementacjach i zdecydował, że musi istnieć lepszy sposób. W związku z tym wprowadzono wspólny wzorzec komunikacji anulowania jako CancellationToken , który został utworzony przy użyciu konstrukcji komunikacji wielowątkowej i międzyprocesowej niższego poziomu. W ramach moich wstępnych badań nad tym wzorcem — i po przejrzeniu rzeczywistego kodu źródłowego platformy .NET do implementacji firmy Microsoft — odkryłem, że CancellationToken może rozwiązać znacznie szerszy zestaw problemów: subskrypcje w stanach uruchamiania aplikacji, limity czasu operacji przy użyciu różnych wyzwalacze i ogólna komunikacja międzyprocesowa za pośrednictwem flag.

Zamierzony przypadek użycia tokena anulowania

CancellationToken został wprowadzony w .NET 4 jako sposób na ulepszenie i standaryzację istniejących rozwiązań do anulowania operacji. Istnieją cztery ogólne podejścia do obsługi anulowania, które są stosowane w popularnych językach programowania:

Zabić Powiedz, nie bierz nie za odpowiedź Zapytaj grzecznie i zaakceptuj odrzucenie Ustaw flagę grzecznie, niech sonduje jeśli chce
Zbliżać się Twardy przystanek; rozwiązywać niespójności później Powiedz mu, żeby przestał, ale niech wszystko posprząta Bezpośrednia, ale delikatna prośba o zatrzymanie Poproś go, aby przestał, ale nie zmuszaj go
Streszczenie Pewna droga do zepsucia i bólu Pozwala na czyste punkty zatrzymania, ale musi się zatrzymać Zezwala na czyste punkty zatrzymania, ale żądanie anulowania może zostać zignorowane Anulowanie jest wymagane przez flagę
Pwątki pthread_kill ,
pthread_cancel (asynchroniczny)
pthread_cancel (tryb odroczony) nie dotyczy Przez flagę
.INTERNET Thread.Abort nie dotyczy Thread.Interrupt Poprzez flagę w CancellationToken
Jawa Thread.destroy ,
Thread.stop
nie dotyczy Thread.interrupt Przez flagę lub Thread.interrupted
Pyton PyThreadState_SetAsyncExc nie dotyczy asyncio.Task.cancel Przez flagę
Przewodnictwo Gorszący; unikaj tego podejścia Dopuszczalne, zwłaszcza gdy język nie obsługuje wyjątków lub rozwijania Dopuszczalne, jeśli język to obsługuje Lepiej, ale bardziej grupowego wysiłku
Podsumowanie podejścia do anulowania i przykłady językowe

CancellationToken znajduje się w końcowej kategorii, w której konwersacja anulowania jest kooperacyjna.

Po tym, jak firma Microsoft wprowadziła CancellationToken , społeczność programistów szybko ją przyjęła, szczególnie dlatego, że wiele głównych interfejsów API platformy .NET zostało zaktualizowanych, aby korzystać z tych tokenów natywnie. Na przykład, począwszy od ASP.NET Core 2,0, akcje obsługują opcjonalny parametr CancellationToken , który może sygnalizować, czy żądanie HTTP zostało zamknięte, co umożliwia anulowanie dowolnej operacji, a tym samym uniknięcie niepotrzebnego użycia zasobów.

Po głębokim zanurzeniu się w bazie kodu platformy .NET stało się jasne, że użycie CancellationToken nie jest ograniczone do anulowania.

Token anulowania pod mikroskopem

Przyglądając się dokładniej implementacji CancellationToken , widzimy, że jest to tylko prosta flaga (tj. ManualResetEvent ) i infrastruktura pomocnicza, która zapewnia możliwość monitorowania i zmiany tej flagi. Główne narzędzie CancellationToken jest w jego nazwie, co sugeruje, że jest to powszechny sposób anulowania operacji. Obecnie każda biblioteka, pakiet lub platforma .NET z operacjami asynchronicznymi lub długotrwałymi umożliwia anulowanie za pomocą tych tokenów.

CancellationToken może zostać wyzwolony przez ręczne ustawienie jego flagi na „true” lub zaprogramowanie go tak, aby zmienił się na „true” po upływie określonego czasu. Niezależnie od sposobu wyzwalania CancellationToken , kod klienta, który monitoruje ten token, może określić wartość flagi tokenu za pomocą jednej z trzech metod:

  • Korzystanie z WaitHandle
  • Odpytywanie flagi CancellationToken
  • Informowanie kodu klienta, gdy stan flagi jest aktualizowany poprzez programową subskrypcję

Po dalszych badaniach w bazie kodu .NET stało się jasne, że zespół .NET uznał CancellationTokens za przydatne w innych scenariuszach niezwiązanych z anulowaniem. Przyjrzyjmy się niektórym z tych zaawansowanych i niezwiązanych z marką przypadków użycia, które umożliwiają deweloperom języka C# koordynację wielowątkową i międzyprocesową w celu uproszczenia złożonych sytuacji.

Tokeny anulowania dla zaawansowanych zdarzeń

Podczas pisania aplikacji ASP.NET Core czasami musimy wiedzieć, kiedy nasza aplikacja została uruchomiona lub musimy wstrzyknąć nasz kod do procesu zamykania hosta. W takich przypadkach korzystamy z interfejsu IHostApplicationLifetime (wcześniej IApplicationLifetime ). Ten interfejs (z repozytorium .NET Core) korzysta z CancellationToken do komunikowania trzech głównych zdarzeń: ApplicationStarted , ApplicationStopping i 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(); } }

Na pierwszy rzut oka może się wydawać, że CancellationToken s nie należą tutaj, zwłaszcza że są używane jako zdarzenia. Jednak dalsze badania pokazują, że te tokeny idealnie pasują:

  • Są elastyczne, umożliwiając klientowi interfejsu na wiele sposobów nasłuchiwanie tych zdarzeń.
  • Są bezpieczne po wyjęciu z pudełka.
  • Można je tworzyć z różnych źródeł, łącząc CancellationToken .

Chociaż CancellationToken nie są idealne dla wszystkich potrzeb związanych ze zdarzeniami, są idealne dla zdarzeń, które mają miejsce tylko raz, takich jak uruchomienie lub zatrzymanie aplikacji.

Token anulowania dla limitu czasu

Domyślnie ASP.NET daje nam bardzo mało czasu na zamknięcie. W tych przypadkach, w których potrzebujemy trochę więcej czasu, użycie wbudowanej klasy HostOptions pozwala nam zmienić wartość tego limitu czasu. Poniżej ta wartość limitu czasu jest opakowana w CancellationToken i przekazywana do podstawowych podprocesów.

IHostedService StopAsync jest doskonałym przykładem tego użycia:

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

Jak widać w definicji interfejsu IHostedService , metoda StopAsync przyjmuje jeden parametr CancellationToken . Komentarz skojarzony z tym parametrem wyraźnie komunikuje początkowy zamiar firmy Microsoft dotyczący CancellationToken jako mechanizmu limitu czasu, a nie procesu anulowania.

Moim zdaniem, gdyby ten interfejs istniał przed istnieniem CancellationToken , mógłby to być parametr TimeSpan — aby wskazać, jak długo operacja zatrzymania mogła zostać przetworzona. Z mojego doświadczenia wynika, że ​​scenariusze przekroczenia limitu czasu prawie zawsze można przekonwertować na CancellationToken z doskonałym dodatkowym narzędziem.

Na razie zapomnijmy, że wiemy, jak zaprojektowano metodę StopAsync , i zamiast tego zastanówmy się, jak zaprojektować kontrakt tej metody. Najpierw zdefiniujmy wymagania:

  • Metoda StopAsync musi spróbować zatrzymać usługę.
  • Metoda StopAsync powinna mieć łagodny stan zatrzymania.
  • Niezależnie od tego, czy zostanie osiągnięty stan bezpiecznego zatrzymania, usługa hostowana musi mieć maksymalny czas zatrzymania, określony przez nasz parametr limitu czasu.

Mając metodę StopAsync w dowolnej formie, spełniamy pierwsze wymaganie. Pozostałe wymagania są trudne. CancellationToken spełnia te wymagania dokładnie przy użyciu standardowego narzędzia komunikacyjnego opartego na flagach platformy .NET, aby wzmocnić konwersację.

Token anulowania jako mechanizm powiadamiania

Największym sekretem CancellationToken jest to, że to tylko flaga. Zilustrujmy, jak CancellationToken może służyć do uruchamiania procesów zamiast ich zatrzymywania.

Rozważ następujące:

  1. Utwórz klasę RandomWorker .
  2. RandomWorker powinien mieć metodę DoWorkAsync , która wykonuje losową pracę.
  3. Metoda DoWorkAsync musi umożliwiać obiektowi wywołującemu określenie, kiedy praca powinna się rozpocząć.
 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); } } }

Powyższa klasa spełnia dwa pierwsze wymagania, pozostawiając nam trzeci. Istnieje kilka alternatywnych interfejsów, których możemy użyć do wyzwalania naszego pracownika, takich jak przedział czasu lub prosta flaga:

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

Te dwa podejścia są w porządku, ale nic nie jest tak eleganckie, jak użycie 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); } } }

Ten przykładowy kod klienta ilustruje moc tego projektu:

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

CancellationTokenSource utworzy nasz CancellationToken za kulisami i koordynuje wyzwalanie wszystkich powiązanych procesów. W tym przypadku powiązanym procesem jest nasz RandomWorker , który czeka na uruchomienie. Takie podejście pozwala nam wykorzystać bezpieczeństwo wątków wypiekane w domyślnej implementacji CancellationToken .

Rozbudowany zestaw narzędzi CancellationToken

Te przykłady pokazują, w jaki sposób CancellationToken udostępnia zestaw narzędzi z rozwiązaniami, które są przydatne poza zamierzonym przypadkiem użycia. Narzędzia mogą być przydatne w wielu scenariuszach, które obejmują komunikację międzyprocesową opartą na flagach. Niezależnie od tego, czy mamy do czynienia z przekroczeniem limitu czasu, powiadomieniami czy zdarzeniami jednorazowymi, możemy polegać na tej eleganckiej, przetestowanej przez Microsoft implementacji.

Od góry do dołu pojawiają się słowa „Złoto” (w kolorze złotym), „Microsoft” i „Partner” (oba w kolorze czarnym), a następnie logo Microsoft.
Jako Złoty Partner Microsoft, Toptal jest Twoją elitarną siecią ekspertów Microsoft. Twórz wysoko wydajne zespoły z ekspertami, których potrzebujesz — w dowolnym miejscu i dokładnie wtedy, gdy ich potrzebujesz!

Dalsza lektura na blogu Toptal Engineering:

  • .NET Core: szaleństwo i otwarte oprogramowanie. Microsoft, co ci zajęło tak długo?!
  • Tworzenie internetowego interfejsu API ASP.NET za pomocą ASP.NET Core
  • Jak uruchamiać i tworzyć projekty .NET