Una guida per programmatori .NET a CancellationToken
Pubblicato: 2022-08-23A 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 |
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:
- Crea una classe
RandomWorker
. -
RandomWorker
dovrebbe avere un metodoDoWorkAsync
che esegua del lavoro casuale. - 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.
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