Una guida per programmatori .NET a CancellationToken

Pubblicato: 2022-08-23

A volte annullare è una buona cosa. In molti dei miei progetti .NET, ho avuto molte motivazioni per annullare sia i processi interni che quelli esterni. Microsoft ha appreso che gli sviluppatori si stavano avvicinando a questo caso d'uso comune in una varietà di implementazioni complesse e ha deciso che doveva esserci un modo migliore. Pertanto, è stato introdotto un modello di comunicazione di annullamento comune come CancellationToken , che è stato creato utilizzando costrutti di comunicazione multithreading e interprocesso di livello inferiore. Come parte della mia ricerca iniziale su questo modello, e dopo aver analizzato l'effettivo codice sorgente .NET per l'implementazione di Microsoft, ho scoperto che CancellationToken può risolvere una serie molto più ampia di problemi: abbonamenti sugli stati di esecuzione delle applicazioni, timeout delle operazioni utilizzando diversi trigger e comunicazioni interprocesso generali tramite flag.

Il caso d'uso previsto per CancellationToken

CancellationToken è stato introdotto in .NET 4 come mezzo per migliorare e standardizzare le soluzioni esistenti per le operazioni di annullamento. Esistono quattro approcci generali alla gestione della cancellazione che i linguaggi di programmazione più diffusi tendono a implementare:

Uccisione Dì, non accettare un no come risposta Chiedi educatamente e accetta il rifiuto Imposta la bandiera educatamente, lascia che esegua il sondaggio se lo desidera
Approccio Arresto duro; risolvere le incongruenze in un secondo momento Digli di fermarsi ma lascia che pulisca le cose Una richiesta diretta ma gentile di fermarsi Chiedigli di fermarsi, ma non forzarlo
Riepilogo Un percorso infallibile verso la corruzione e il dolore Consente punti di arresto puliti ma deve arrestarsi Consente punti di arresto puliti, ma la richiesta di annullamento potrebbe essere ignorata La cancellazione è richiesta tramite un flag
Pthread pthread_kill ,
pthread_cancel (asincrono)
pthread_cancel (modalità differita) n / a Attraverso una bandiera
.RETE Thread.Abort n / a Thread.Interrupt Attraverso un flag in CancellationToken
Giava Thread.destroy ,
Thread.stop
n / a Thread.interrupt Attraverso un flag o Thread.interrupted
Pitone PyThreadState_SetAsyncExc n / a asyncio.Task.cancel Attraverso una bandiera
Guida Inaccettabile; evitare questo approccio Accettabile, soprattutto quando una lingua non supporta eccezioni o unwinding Accettabile se la lingua lo supporta Meglio, ma più uno sforzo di gruppo
Riepilogo dell'approccio alla cancellazione ed esempi linguistici

CancellationToken risiede nella categoria finale, dove la conversazione di annullamento è cooperativa.

Dopo che Microsoft ha introdotto CancellationToken , la comunità di sviluppo l'ha rapidamente accolto, in particolare perché molte delle principali API .NET sono state aggiornate per utilizzare questi token in modo nativo. Ad esempio, a partire da ASP.NET Core 2,0, le azioni supportano un parametro CancellationToken facoltativo che può segnalare se una richiesta HTTP è stata chiusa, consentendo l'annullamento di qualsiasi operazione ed evitando così l'uso inutile delle risorse.

Dopo un'analisi approfondita della base di codice .NET, è diventato chiaro che l'utilizzo di CancellationToken non si limita all'annullamento.

CancellationToken al microscopio

Osservando più da vicino l'implementazione di CancellationToken , vediamo che è solo un semplice flag (ad esempio ManualResetEvent ) e l'infrastruttura di supporto che offre la possibilità di monitorare e modificare quel flag. L'utilità principale di CancellationToken è nel suo nome, il che suggerisce che questo è il modo comune per annullare le operazioni. Al giorno d'oggi, qualsiasi libreria, pacchetto o framework .NET con operazioni asincrone o di lunga durata consente l'annullamento tramite questi token.

CancellationToken può essere attivato impostando manualmente il suo flag su "true" o programmandolo in modo che cambi in "true" dopo che è trascorso un certo intervallo di tempo. Indipendentemente dal modo in cui viene attivato CancellationToken , il codice client che sta monitorando questo token può determinare il valore del flag del token tramite uno dei tre metodi seguenti:

  • Utilizzo di un WaitHandle
  • Polling del flag di CancellationToken
  • Informare il codice client quando lo stato del flag viene aggiornato tramite una sottoscrizione programmatica

Dopo ulteriori ricerche nella base di codice .NET, è diventato evidente che il team .NET ha trovato CancellationTokens utili in altri scenari non collegati all'annullamento. Esaminiamo alcuni di questi casi d'uso avanzati e fuori marchio, che consentono agli sviluppatori C# di coordinare multithread e tra processi per semplificare situazioni complesse.

CancellationToken per eventi avanzati

Quando si scrivono applicazioni ASP.NET Core, a volte è necessario sapere quando è stata avviata l'applicazione oppure è necessario inserire il codice nel processo di arresto dell'host. In questi casi, utilizziamo l'interfaccia IHostApplicationLifetime (in precedenza IApplicationLifetime ). Questa interfaccia (dal repository di .NET Core) utilizza CancellationToken per comunicare tre eventi principali: ApplicationStarted , ApplicationStopping e 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(); } }

A prima vista, potrebbe sembrare che i CancellationToken non appartengano qui, soprattutto perché vengono usati come eventi. Tuttavia, un ulteriore esame rivela che questi token si adattano perfettamente:

  • Sono flessibili e consentono al client dell'interfaccia di ascoltare questi eventi in diversi modi.
  • Sono thread-safe fuori dagli schemi.
  • Possono essere creati da diverse fonti combinando CancellationToken s.

Sebbene i CancellationToken non siano perfetti per ogni esigenza di eventi, sono ideali per eventi che si verificano solo una volta, come l'avvio o l'arresto dell'applicazione.

CancellationToken per Timeout

Per impostazione predefinita, ASP.NET ci offre pochissimo tempo per l'arresto. Nei casi in cui desideriamo un po' più di tempo, l'utilizzo della classe HostOptions ci consente di modificare questo valore di timeout. Sotto, questo valore di timeout viene racchiuso in un CancellationToken e inserito nei sottoprocessi sottostanti.

Il metodo IHostedService di StopAsync è un ottimo esempio di questo utilizzo:

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

Come evidente nella definizione dell'interfaccia IHostedService , il metodo StopAsync accetta un parametro CancellationToken . Il commento associato a quel parametro comunica chiaramente che l'intento iniziale di Microsoft per CancellationToken era un meccanismo di timeout piuttosto che un processo di annullamento.

A mio parere, se questa interfaccia fosse esistita prima dell'esistenza di CancellationToken , questo avrebbe potuto essere un parametro TimeSpan , per indicare per quanto tempo era consentita l'elaborazione dell'operazione di arresto. Nella mia esperienza, gli scenari di timeout possono quasi sempre essere convertiti in un CancellationToken con una grande utilità aggiuntiva.

Per il momento, dimentichiamo che sappiamo come è progettato il metodo StopAsync e pensiamo invece a come vorremmo progettare il contratto di questo metodo. Per prima cosa definiamo i requisiti:

  • Il metodo StopAsync deve tentare di arrestare il servizio.
  • Il metodo StopAsync dovrebbe avere uno stato di arresto normale.
  • Indipendentemente dal fatto che venga raggiunto uno stato di arresto regolare, un servizio ospitato deve avere un tempo massimo per l'arresto, come definito dal nostro parametro di timeout.

Avendo un metodo StopAsync in qualsiasi forma, soddisfiamo il primo requisito. I restanti requisiti sono complicati. CancellationToken soddisfa esattamente questi requisiti utilizzando uno strumento di comunicazione standard basato su flag .NET per potenziare la conversazione.

CancellationToken come meccanismo di notifica

Il più grande segreto dietro CancellationToken è che è solo una bandiera. Illustriamo come è possibile utilizzare CancellationToken per avviare processi invece di interromperli.

Considera quanto segue:

  1. Crea una classe RandomWorker .
  2. RandomWorker dovrebbe avere un metodo DoWorkAsync che esegua del lavoro casuale.
  3. Il metodo DoWorkAsync deve consentire a un chiamante di specificare quando deve iniziare il lavoro.
 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 di cui sopra soddisfa i primi due requisiti, lasciandoci al terzo. Esistono diverse interfacce alternative che potremmo utilizzare per attivare il nostro lavoratore, come un intervallo di tempo o un semplice flag:

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

Questi due approcci vanno bene, ma niente è elegante come usare 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); } } }

Questo codice client di esempio illustra la potenza di questo design:

 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 creerà il nostro CancellationToken dietro le quinte e coordinerà l'attivazione di tutti i processi associati. In questo caso, il processo associato è il nostro RandomWorker , che è in attesa di iniziare. Questo approccio ci consente di sfruttare la sicurezza del thread incorporata nell'implementazione predefinita di CancellationToken .

Un'ampia cassetta degli attrezzi CancellationToken

Questi esempi dimostrano come CancellationToken fornisce un toolbox di soluzioni utili al di fuori del caso d'uso previsto. Gli strumenti possono tornare utili in molti scenari che coinvolgono la comunicazione interprocesso basata su flag. Indipendentemente dal fatto che ci troviamo di fronte a timeout, notifiche o eventi occasionali, possiamo ricorrere a questa elegante implementazione testata da Microsoft.

Dall'alto verso il basso, vengono visualizzate le parole "Gold" (color oro), "Microsoft" e "Partner" (entrambe in nero) seguite dal logo Microsoft.
In qualità di Microsoft Gold Partner, Toptal è la tua rete d'élite di esperti Microsoft. Crea team ad alte prestazioni con gli esperti di cui hai bisogno, ovunque ed esattamente quando ne hai bisogno!

Ulteriori letture sul blog di Toptal Engineering:

  • .NET Core: scatenarsi e open source. Microsoft, perché ci hai messo così tanto?!
  • Creazione di un'API Web ASP.NET con ASP.NET Core
  • Come avviare e creare progetti .NET