Ghid pentru programator .NET pentru CancellationToken

Publicat: 2022-08-23

Uneori, anularea este un lucru bun. În multe dintre proiectele mele .NET, am avut destulă motivație să anulez atât procesele interne, cât și cele externe. Microsoft a aflat că dezvoltatorii abordează acest caz comun de utilizare într-o varietate de implementări complexe și au decis că trebuie să existe o modalitate mai bună. Astfel, a fost introdus un model comun de comunicare de anulare ca CancellationToken , care a fost construit folosind constructe de comunicare interprocese și multithreading de nivel inferior. Ca parte a cercetării mele inițiale asupra acestui tipar – și după ce am săpat prin codul sursă .NET real pentru implementarea Microsoft – am descoperit că CancellationToken poate rezolva un set mult mai larg de probleme: abonamente la stările de rulare ale aplicațiilor, time-out operațiuni folosind diferite declanșatoare și comunicații generale între procese prin intermediul steagurilor.

Cazul de utilizare prevăzut pentru CancellationToken

CancellationToken a fost introdus în .NET 4 ca mijloc de îmbunătățire și standardizare a soluțiilor existente pentru anularea operațiunilor. Există patru abordări generale pentru gestionarea anulării pe care limbajele de programare populare tind să le implementeze:

Ucide Spune, nu accepta un nu ca răspuns Întrebați politicos și acceptați respingerea Setați steag politicos, lăsați-l să facă sondaj dacă dorește
Abordare oprire grea; rezolva inconsecvențele mai târziu Spune-i să se oprească, dar lasă-l să curețe lucrurile O cerere directă, dar blândă de a opri Cere-i să se oprească, dar nu-l forța
rezumat O cale sigură către corupție și durere Permite puncte de oprire curate, dar trebuie să se oprească Permite puncte de oprire curate, dar cererea de anulare poate fi ignorată Anularea se solicită printr-un steag
Pthreads pthread_kill ,
pthread_cancel (async)
pthread_cancel (mod amânat) N / A Printr-un steag
.NET Thread.Abort N / A Thread.Interrupt Printr-un steag în CancellationToken
Java Thread.destroy ,
Thread.stop
N / A Thread.interrupt Printr-un steag sau Thread.interrupted
Piton PyThreadState_SetAsyncExc N / A asyncio.Task.cancel Printr-un steag
Îndrumare Inacceptabil; evitați această abordare Acceptabil, mai ales atunci când o limbă nu acceptă excepții sau derulare Acceptabil dacă limba o acceptă Mai bine, dar mai mult efort de grup
Rezumatul abordării privind anularea și exemplele de limbă

CancellationToken se află în categoria finală, unde conversația de anulare este cooperantă.

După ce Microsoft a introdus CancellationToken , comunitatea de dezvoltare l-a îmbrățișat rapid, în special pentru că multe API-uri .NET majore au fost actualizate pentru a utiliza aceste token-uri în mod nativ. De exemplu, începând cu ASP.NET Core 2.0, acțiunile acceptă un parametru opțional CancellationToken care poate semnala dacă o solicitare HTTP a fost închisă, permițând anularea oricărei operațiuni și evitând astfel utilizarea inutilă a resurselor.

După o scufundare profundă în baza de cod .NET, a devenit clar că utilizarea CancellationToken nu se limitează la anulare.

CancellationToken sub microscop

Când ne uităm mai îndeaproape la implementarea CancellationToken , vedem că este doar un simplu semnal (adică, ManualResetEvent ) și infrastructura de suport care oferă posibilitatea de a monitoriza și schimba acel semnal. Principalul utilitar al CancellationToken se află în numele său, ceea ce sugerează că aceasta este modalitatea obișnuită de a anula operațiunile. În zilele noastre, orice bibliotecă, pachet sau cadru .NET cu operațiuni asincrone sau de lungă durată permite anularea prin aceste token-uri.

CancellationToken poate fi declanșat fie prin setarea manuală a steagului său la „adevărat”, fie prin programarea acestuia să se schimbe în „adevărat” după ce a trecut un anumit interval de timp. Indiferent de modul în care este declanșat un CancellationToken , codul client care monitorizează acest jeton poate determina valoarea semnalizatorului jetonului prin una dintre cele trei metode:

  • Folosind un WaitHandle
  • Sondajul semnalului CancellationToken
  • Informarea codului clientului când starea steagului este actualizată printr-un abonament programatic

După cercetări suplimentare în baza de cod .NET, a devenit evident că echipa .NET a găsit CancellationTokens utile în alte scenarii care nu au legătură cu anularea. Să explorăm câteva dintre aceste cazuri de utilizare avansate și în afara mărcii, care oferă dezvoltatorilor C# o coordonare multithreaded și interproces pentru a simplifica situațiile complexe.

Jetoane de anulare pentru evenimente avansate

Când scriem aplicații ASP.NET Core, uneori trebuie să știm când a pornit aplicația noastră sau trebuie să ne injectăm codul în procesul de oprire a gazdei. În aceste cazuri, folosim interfața IHostApplicationLifetime (anterior IApplicationLifetime ). Această interfață (din depozitul .NET Core) folosește CancellationToken pentru a comunica trei evenimente majore: 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(); } }

La prima vedere, poate părea că CancellationToken -urile nu aparțin aici, mai ales că sunt folosite ca evenimente. Cu toate acestea, o examinare suplimentară dezvăluie că aceste jetoane se potrivesc perfect:

  • Sunt flexibile, permițând mai multe moduri pentru clientul interfeței de a asculta aceste evenimente.
  • Sunt sigure pentru fir din cutie.
  • Ele pot fi create din diferite surse prin combinarea CancellationToken -uri.

Deși CancellationToken -urile nu sunt perfecte pentru orice nevoie de eveniment, ele sunt ideale pentru evenimente care au loc o singură dată, cum ar fi pornirea sau oprirea aplicației.

CancellationToken pentru Timeout

În mod implicit, ASP.NET ne oferă foarte puțin timp pentru a ne închide. În acele cazuri în care dorim puțin mai mult timp, utilizarea clasei HostOptions ne permite să schimbăm această valoare de timeout. Dedesubt, această valoare de timeout este înfășurată într-un CancellationToken și introdusă în subprocesele subiacente.

IHostedService a lui StopAsync este un exemplu excelent al acestei utilizări:

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

După cum este evident în definiția interfeței IHostedService , metoda StopAsync ia un parametru CancellationToken . Comentariul asociat cu acel parametru comunică în mod clar intenția inițială a Microsoft pentru CancellationToken , mai degrabă ca un mecanism de timeout decât un proces de anulare.

După părerea mea, dacă această interfață ar fi existat înainte de existența lui CancellationToken , acesta ar fi putut fi un parametru TimeSpan - pentru a indica cât timp a fost permisă procesarea operațiunii de oprire. Din experiența mea, scenariile de timeout pot fi aproape întotdeauna convertite într-un CancellationToken cu o mare utilitate suplimentară.

Pentru moment, să uităm că știm cum este concepută metoda StopAsync și, în schimb, să ne gândim cum am proiecta contractul acestei metode. Mai întâi să definim cerințele:

  • Metoda StopAsync trebuie să încerce să oprească serviciul.
  • Metoda StopAsync ar trebui să aibă o stare de oprire grațioasă.
  • Indiferent dacă se realizează o stare de oprire grațioasă, un serviciu găzduit trebuie să aibă un timp maxim în care să se oprească, așa cum este definit de parametrul nostru de expirare.

Având o metodă StopAsync sub orice formă, îndeplinim prima cerință. Restul cerințelor sunt complicate. CancellationToken satisface aceste cerințe exact prin utilizarea unui instrument standard de comunicare bazat pe flag .NET pentru a împuternici conversația.

CancellationToken ca mecanism de notificare

Cel mai mare secret din spatele CancellationToken este că este doar un steag. Să ilustrăm modul în care CancellationToken poate fi folosit pentru a porni procesele în loc să le oprească.

Luați în considerare următoarele:

  1. Creați o clasă RandomWorker .
  2. RandomWorker ar trebui să aibă o metodă DoWorkAsync care execută unele lucrări aleatorii.
  3. Metoda DoWorkAsync trebuie să permită unui apelant să specifice când ar trebui să înceapă lucrul.
 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); } } }

Clasa de mai sus satisface primele două cerințe, lăsându-ne cu a treia. Există mai multe interfețe alternative pe care le-am putea folosi pentru a declanșa lucrătorul nostru, cum ar fi un interval de timp sau un simplu semnalizare:

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

Aceste două abordări sunt bune, dar nimic nu este la fel de elegant ca utilizarea unui 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); } } }

Acest exemplu de cod client ilustrează puterea acestui 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 va crea CancellationToken -ul nostru în culise și va coordona declanșarea tuturor proceselor asociate. În acest caz, procesul asociat este RandomWorker , care așteaptă să înceapă. Această abordare ne permite să valorificăm siguranța firului inclusă în implementarea implicită CancellationToken .

O cutie de instrumente extinsă CancellationToken

Aceste exemple demonstrează modul în care CancellationToken oferă o cutie de instrumente de soluții care sunt utile în afara cazului de utilizare prevăzut. Instrumentele pot fi utile în multe scenarii care implică comunicarea interproceselor bazată pe flag. Indiferent dacă ne confruntăm cu timeout-uri, notificări sau evenimente unice, ne putem întoarce la această implementare elegantă, testată de Microsoft.

De sus în jos, cuvintele „Gold” (aur colorat), „Microsoft” și „Partner” (ambele în negru) apar urmate de sigla Microsoft.
În calitate de partener de aur Microsoft, Toptal este rețeaua dvs. de elită de experți Microsoft. Construiți echipe de înaltă performanță cu experții de care aveți nevoie, oriunde și exact când aveți nevoie de ei!

Citiți suplimentare pe blogul Toptal Engineering:

  • .NET Core: Going Wild și Open Source. Microsoft, ce ți-a luat atât de mult?!
  • Construirea unui API Web ASP.NET cu ASP.NET Core
  • Cum să pornești și să creezi proiecte .NET