Ghid pentru programator .NET pentru CancellationToken
Publicat: 2022-08-23Uneori, 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 |
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:
- Creați o clasă
RandomWorker
. -
RandomWorker
ar trebui să aibă o metodăDoWorkAsync
care execută unele lucrări aleatorii. - 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.
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