Przewodnik programisty .NET dotyczący CancellationToken
Opublikowany: 2022-08-23Czasami 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 |
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:
- Utwórz klasę
RandomWorker
. -
RandomWorker
powinien mieć metodęDoWorkAsync
, która wykonuje losową pracę. - 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.
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