.NET: rīki darbam ar daudzpavedienu un asinhroniju. 1. daļa
PublicÄju oriÄ£inÄlrakstu par Habr, kura tulkojums ir ievietots korporatÄ«vajÄ emuÄra ziÅa.
NepiecieÅ”amÄ«ba kaut ko darÄ«t asinhroni, negaidot rezultÄtu Å”eit un tagad, vai sadalÄ«t lielu darbu starp vairÄkÄm vienÄ«bÄm, kas to veic, pastÄvÄja pirms datoru parÄdÄ«Å”anÄs. LÄ«dz ar viÅu parÄdÄ«Å”anos Ŕī vajadzÄ«ba kļuva ļoti taustÄma. Tagad, 2019. gadÄ, es rakstu Å”o rakstu klÄpjdatorÄ ar 8 kodolu Intel Core procesoru, kurÄ paralÄli darbojas vairÄk nekÄ simts procesu un vÄl vairÄk pavedienu. Blakus ir nedaudz nobružÄts telefons, pirkts pirms pÄris gadiem, tam ir 8 kodolu procesors. Tematiskajos resursos ir daudz rakstu un videoklipu, kuros to autori apbrÄ«no Ŕī gada vadoÅ”os viedtÄlruÅus ar 16 kodolu procesoriem. MS Azure nodroÅ”ina virtuÄlo maŔīnu ar 20 kodolu procesoru un 128 TB RAM par mazÄk nekÄ 2 USD stundÄ. DiemžÄl nav iespÄjams iegÅ«t maksimumu un izmantot Å”o spÄku, nespÄjot pÄrvaldÄ«t pavedienu mijiedarbÄ«bu.
VÄrdu krÄjums
Process - OS objekts, izolÄta adreÅ”u telpa, satur pavedienus. Pavediens - OS objekts, mazÄkÄ izpildes vienÄ«ba, procesa daļa, pavedieni procesa ietvaros savÄ starpÄ dala atmiÅu un citus resursus. Daudzuzdevumu veikÅ”ana - OS Ä«paÅ”ums, iespÄja palaist vairÄkus procesus vienlaicÄ«gi Daudzkodolu - procesora Ä«paŔība, iespÄja datu apstrÄdei izmantot vairÄkus kodolus DaudzkÄrtÄja apstrÄde - datora Ä«paŔība, spÄja vienlaikus fiziski strÄdÄt ar vairÄkiem procesoriem Daudzpavedienu veidoÅ”ana ā procesa Ä«paŔība, iespÄja sadalÄ«t datu apstrÄdi starp vairÄkiem pavedieniem. ParalÄlisms - veicot vairÄkas darbÄ«bas fiziski vienlaicÄ«gi laika vienÄ«bÄ Asinhronija - operÄcijas izpilde, negaidot Ŕīs apstrÄdes pabeigÅ”anu; izpildes rezultÄtu var apstrÄdÄt vÄlÄk.
Metafora
Ne visas definÄ«cijas ir labas, un dažÄm ir nepiecieÅ”ams papildu skaidrojums, tÄpÄc formÄli ieviestajai terminoloÄ£ijai pievienoÅ”u metaforu par brokastu gatavoÅ”anu. Brokastu gatavoÅ”ana Å”ajÄ metaforÄ ir process.
No rÄ«ta gatavojot brokastis es (CPU) Es nÄku uz virtuvi (Dators). Man ir 2 rokas (KrÄsas). VirtuvÄ ir vairÄkas ierÄ«ces (IO): cepeÅ”krÄsns, tÄjkanna, tosteris, ledusskapis. Es ieslÄdzu gÄzi, uzlieku pannu un ieleju tajÄ eļļu, negaidot, kad tÄ uzkarst (asinhroni, Non-Blocking-IO-Wait), es izÅemu olas no ledusskapja un sadalu tÄs ŔķīvÄ«, tad sasitu ar vienu roku (Pavediens #1), un otrais (Pavediens #2) turot plÄksni (koplietotais resurss). Tagad es gribÄtu ieslÄgt tÄjkannu, bet man nepietiek roku (Pavediens Bads) Å ajÄ laikÄ uzsilst panna (ApstrÄdÄjot rezultÄtu), kurÄ ieleju to, ko esmu saputojusi. Es sniedzos pÄc tÄjkannas un ieslÄdzu to un stulbi skatos, kÄ tajÄ vÄrÄs Å«dens (BloÄ·ÄÅ”ana-IO-pagaidiet), lai gan Å”ajÄ laikÄ viÅÅ” bÅ«tu varÄjis izmazgÄt Ŕķīvi, kurÄ saputoja omleti.
Es gatavoju omleti izmantojot tikai 2 rokas, un vairÄk man nav, bet tajÄ paÅ”Ä laikÄ omletes putoÅ”anas brÄ«dÄ« notika uzreiz 3 operÄcijas: omletes saputoÅ”ana, Ŕķīvja turÄÅ”ana, pannas uzkarsÄÅ”ana. CPU ir datora ÄtrÄkÄ daļa, IO ir tas, kas visbiežÄk viss palÄninÄs, tÄpÄc bieži vien efektÄ«vs risinÄjums ir CPU ar kaut ko aizÅemt, saÅemot datus no IO.
Turpinot metaforu:
Ja omletes gatavoÅ”anas procesÄ mÄÄ£inÄtu arÄ« pÄrÄ£Ärbties, Å”is bÅ«tu vairÄkuzdevumu piemÄrs. SvarÄ«ga nianse: datori Å”ajÄ ziÅÄ ir daudz labÄki nekÄ cilvÄki.
Virtuve ar vairÄkiem Å”efpavÄriem, piemÄram, restorÄnÄ - daudzkodolu dators.
Daudzi restorÄni pÄrtikas laukumÄ tirdzniecÄ«bas centrÄ - datu centrÄ
.NET rīki
.NET labi strÄdÄ ar pavedieniem, tÄpat kÄ ar daudzÄm citÄm lietÄm. Ar katru jauno versiju tas ievieÅ” arvien jaunus rÄ«kus darbam ar tiem, jaunus abstrakcijas slÄÅus OS pavedienos. StrÄdÄjot ar abstrakciju konstruÄÅ”anu, ietvara izstrÄdÄtÄji izmanto pieeju, kas, izmantojot augsta lÄ«meÅa abstrakciju, atstÄj iespÄju pazeminÄt vienu vai vairÄkus lÄ«meÅus zemÄk. VisbiežÄk tas nav nepiecieÅ”ams, patiesÄ«bÄ tas paver durvis Å”auÅ”anai sev kÄjÄ ar bisi, bet dažreiz, retos gadÄ«jumos, tas var bÅ«t vienÄ«gais veids, kÄ atrisinÄt problÄmu, kas nav atrisinÄta paÅ”reizÄjÄ abstrakcijas lÄ«menÄ« .
Ar rÄ«kiem es domÄju gan ietvarprogrammu saskarnes (API), ko nodroÅ”ina ietvars, gan treÅ”o puÅ”u pakotnes, kÄ arÄ« veselus programmatÅ«ras risinÄjumus, kas vienkÄrÅ”o ar daudzpavedienu kodu saistÄ«to problÄmu meklÄÅ”anu.
Pavediena uzsÄkÅ”ana
Pavedienu klase ir visvienkÄrÅ”ÄkÄ .NET klase darbam ar pavedieniem. Konstruktors pieÅem vienu no diviem delegÄtiem:
ThreadStart ā nav parametru
ParametrizedThreadStart - ar vienu objekta tipa parametru.
DelegÄts tiks izpildÄ«ts jaunizveidotajÄ pavedienÄ pÄc Start metodes izsaukÅ”anas Ja konstruktoram tika nodots ParametrizedThreadStart tipa delegÄts, tad SÄkt metodei ir jÄnodod objekts. Å is mehÄnisms ir nepiecieÅ”ams, lai pÄrsÅ«tÄ«tu uz straumi jebkÄdu vietÄjo informÄciju. Ir vÄrts atzÄ«mÄt, ka pavediena izveide ir dÄrga darbÄ«ba, un pavediens pats par sevi ir smags objekts, vismaz tÄpÄc, ka tas stekÄ atvÄl 1 MB atmiÅas un prasa mijiedarbÄ«bu ar OS API.
new Thread(...).Start(...);
ThreadPool klase atspoguļo baseina jÄdzienu. NET tÄ«klÄ pavedienu kopums ir inženierijas darbs, un Microsoft izstrÄdÄtÄji ir ieguldÄ«juÅ”i daudz pūļu, lai nodroÅ”inÄtu, ka tas darbojas optimÄli dažÄdos scenÄrijos.
VispÄrÄjÄ koncepcija:
No lietojumprogrammas palaiÅ”anas brīža tÄ fonÄ izveido vairÄkus pavedienus rezervÄ un nodroÅ”ina iespÄju Åemt tos lietoÅ”anai. Ja pavedieni tiek izmantoti bieži un lielÄ skaitÄ, kopums paplaÅ”inÄs, lai apmierinÄtu zvanÄ«tÄja vajadzÄ«bas. Ja Ä«stajÄ laikÄ baseinÄ nav brÄ«vu pavedienu, tas vai nu gaidÄ«s, lÄ«dz kÄds no pavedieniem atgriezÄ«sies, vai arÄ« izveidos jaunu. No tÄ izriet, ka pavedienu pÅ«ls ir lieliski piemÄrots dažÄm Ä«stermiÅa darbÄ«bÄm un ir slikti piemÄrots darbÄ«bÄm, kas darbojas kÄ pakalpojumi visÄ lietojumprogrammas darbÄ«bas laikÄ.
Lai izmantotu pavedienu no pÅ«la, ir metode QueueUserWorkItem, kas pieÅem WaitCallback tipa delegÄtu, kuram ir tÄds pats paraksts kÄ ParametrizedThreadStart, un tam nodotais parametrs veic to paÅ”u funkciju.
ThreadPool.QueueUserWorkItem(...);
MazÄk zinÄmÄ pavedienu kopas metode RegisterWaitForSingleObject tiek izmantota, lai organizÄtu nebloÄ·ÄjoÅ”as IO darbÄ«bas. Å ai metodei nodotais pÄrstÄvis tiks izsaukts, kad metodei nodotais WaitHandle ir āAtbrÄ«votsā.
ThreadPool.RegisterWaitForSingleObject(...)
.NET ir pavedienu taimeris, un tas atŔķiras no WinForms/WPF taimeriem ar to, ka tÄ apstrÄdÄtÄjs tiks izsaukts pavedienÄ, kas Åemts no pÅ«la.
System.Threading.Timer
Ir arÄ« diezgan eksotisks veids, kÄ nosÅ«tÄ«t delegÄtu izpildei uz pavedienu no pÅ«la - metode BeginInvoke.
DelegateInstance.BeginInvoke
Es vÄlos Ä«si pakavÄties pie funkcijas, uz kuru var izsaukt daudzas no iepriekÅ”minÄtajÄm metodÄm - CreateThread no Kernel32.dll Win32 API. Pateicoties ÄrÄjo metožu mehÄnismam, ir iespÄja izsaukt Å”o funkciju. Å Ädu aicinÄjumu esmu redzÄjis tikai vienu reizi Å”ausmÄ«gÄ mantotÄ koda piemÄrÄ, un autora motivÄcija, kurÅ” tieÅ”i to izdarÄ«ja, man joprojÄm ir noslÄpums.
Kernel32.dll CreateThread
Pavedienu skatīŔana un atkļūdoŔana
JÅ«su izveidotos pavedienus, visus treÅ”o puÅ”u komponentus un .NET kopu var skatÄ«t Visual Studio logÄ Threads. Å ajÄ logÄ tiks parÄdÄ«ta informÄcija par pavedienu tikai tad, ja lietojumprogramma atrodas atkļūdoÅ”anas režīmÄ un pÄrtraukuma režīmÄ. Å eit varat Ärti apskatÄ«t katra pavediena steku nosaukumus un prioritÄtes, kÄ arÄ« pÄrslÄgt atkļūdoÅ”anu uz noteiktu pavedienu. Izmantojot klases Thread Ä«paŔību Priority, varat iestatÄ«t pavediena prioritÄti, ko OC un CLR uztvers kÄ ieteikumu, sadalot procesora laiku starp pavedieniem.
Uzdevumu paralÄlÄ bibliotÄka
Uzdevumu paralÄlÄ bibliotÄka (TPL) tika ieviesta .NET 4.0. Tagad tas ir standarts un galvenais rÄ«ks darbam ar asinhroniju. JebkurÅ” kods, kas izmanto vecÄku pieeju, tiek uzskatÄ«ts par mantotu. TPL pamatvienÄ«ba ir Task klase no System.Threading.Tasks nosaukumvietas. Uzdevums ir abstrakcija pÄr pavedienu. Ar jauno C# valodas versiju mÄs ieguvÄm elegantu veidu, kÄ strÄdÄt ar uzdevumiem - async/await operatoriem. Å Ä«s koncepcijas ļÄva rakstÄ«t asinhrono kodu tÄ, it kÄ tas bÅ«tu vienkÄrÅ”s un sinhrons, un tas ļÄva pat cilvÄkiem, kuri maz saprot pavedienu iekÅ”Äjo darbÄ«bu, rakstÄ«t lietojumprogrammas, kas tos izmanto, lietojumprogrammas, kas nesasaldÄ, veicot ilgas darbÄ«bas. Asinhronas/gaidÄ«Å”anas izmantoÅ”ana ir viena vai pat vairÄku rakstu tÄma, taÄu es mÄÄ£inÄÅ”u izprast tÄ bÅ«tÄ«bu dažos teikumos:
async ir modifikators metodei, kas atgriež uzdevumu vai tukŔumu
un gaida ir nebloÄ·ÄjoÅ”s Uzdevuma gaidÄ«Å”anas operators.
VÄlreiz: operators await vispÄrÄ«gÄ gadÄ«jumÄ (ir izÅÄmumi) atbrÄ«vos paÅ”reizÄjo izpildes pavedienu tÄlÄk, un, kad uzdevums pabeigs izpildi, un pavedienu (patiesÄ«bÄ pareizÄk bÅ«tu teikt kontekstu , bet vairÄk par to vÄlÄk) turpinÄs Ŕīs metodes izpildi. NET iekÅ”ienÄ Å”is mehÄnisms tiek realizÄts tÄpat kÄ peļÅas atdeve, kad rakstÄ«tÄ metode pÄrvÄrÅ”as par veselu klasi, kas ir stÄvokļa maŔīna un atkarÄ«bÄ no Å”iem stÄvokļiem var tikt izpildÄ«ta atseviŔķos gabalos. Ikviens interesents var uzrakstÄ«t jebkuru vienkÄrÅ”u kodu, izmantojot asynŃ/await, apkopot un apskatÄ«t komplektÄciju, izmantojot JetBrains dotPeek ar iespÄjotu kompilatora Ä£enerÄto kodu.
ApskatÄ«sim opcijas Task palaiÅ”anai un lietoÅ”anai. TÄlÄk esoÅ”ajÄ koda piemÄrÄ mÄs izveidojam jaunu uzdevumu, kas nedara neko noderÄ«gu (Pavediens.Miega režīms (10000)), taÄu reÄlajÄ dzÄ«vÄ tam vajadzÄtu bÅ«t sarežģītam CPU intensÄ«vam darbam.
using TCO = System.Threading.Tasks.TaskCreationOptions;
public static async void VoidAsyncMethod() {
var cancellationSource = new CancellationTokenSource();
await Task.Factory.StartNew(
// Code of action will be executed on other context
() => Thread.Sleep(10000),
cancellationSource.Token,
TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness,
scheduler
);
// Code after await will be executed on captured context
}
Tiek izveidots uzdevums ar vairÄkÄm opcijÄm:
LongRunning ir mÄjiens, ka uzdevums netiks pabeigts Ätri, un tas nozÄ«mÄ, ka ir vÄrts apsvÄrt iespÄju neÅemt pavedienu no pÅ«la, bet izveidot Å”im uzdevumam atseviŔķu pavedienu, lai nekaitÄtu citiem.
AttachedToParent ā uzdevumus var sakÄrtot hierarhijÄ. Ja Ŕī opcija tika izmantota, tad uzdevums var bÅ«t stÄvoklÄ«, kurÄ tas pats ir pabeigts un gaida savu bÄrnu izpildi.
PreferFairness - nozÄ«mÄ, ka bÅ«tu labÄk izpildÄ«t izpildei nosÅ«tÄ«tos uzdevumus agrÄk, nevis vÄlÄk nosÅ«tÄ«tos. Bet tas ir tikai ieteikums, un rezultÄti nav garantÄti.
Otrais parametrs, kas tiek nodots metodei, ir CancellationToken. Lai pareizi apstrÄdÄtu darbÄ«bas atcelÅ”anu pÄc tÄs sÄkÅ”anas, izpildÄmais kods ir jÄaizpilda ar CancellationToken stÄvokļa pÄrbaudÄm. Ja pÄrbaudes nav, tad CancellationTokenSource objekta izsauktÄ Cancel metode varÄs apturÄt uzdevuma izpildi tikai pirms tÄ sÄkÅ”anas.
PÄdÄjais parametrs ir TaskScheduler tipa plÄnotÄja objekts. Å Ä« klase un tÄs pÄcteÄi ir paredzÄti, lai kontrolÄtu stratÄÄ£ijas uzdevumu sadalei pa pavedieniem; pÄc noklusÄjuma uzdevums tiks izpildÄ«ts izlases pavedienÄ no kopas.
Izveidotajam uzdevumam tiek pielietots await operators, kas nozÄ«mÄ, ka aiz tÄ rakstÄ«tais kods, ja tÄds ir, tiks izpildÄ«ts tajÄ paÅ”Ä kontekstÄ (bieži vien tas nozÄ«mÄ tajÄ paÅ”Ä pavedienÄ) kÄ kods pirms await.
Metode ir atzÄ«mÄta kÄ asinhrona spÄkÄ neesoÅ”a, kas nozÄ«mÄ, ka tÄ var izmantot gaidÄ«Å”anas operatoru, bet izsaucoÅ”ais kods nevarÄs gaidÄ«t izpildi. Ja Å”Äda funkcija ir nepiecieÅ”ama, metodei ir jÄatgriež uzdevums. Metodes, kas apzÄ«mÄtas ar asinhronu tukÅ”umu, ir diezgan izplatÄ«tas: parasti tÄs ir notikumu apstrÄdÄtÄji vai citas metodes, kas darbojas uz uguns un aizmirst principu. Ja jums ir nepiecieÅ”ams ne tikai dot iespÄju gaidÄ«t lÄ«dz izpildes beigÄm, bet arÄ« atgriezt rezultÄtu, tad jums ir jÄizmanto Task.
UzdevumÄ, ko atgrieza metode StartNew, kÄ arÄ« jebkurÄ citÄ, varat izsaukt ConfigureAwait metodi ar viltus parametru, tad izpilde pÄc gaidÄ«Å”anas turpinÄsies nevis uzÅemtajÄ kontekstÄ, bet gan patvaļīgÄ kontekstÄ. Tas jÄdara vienmÄr, ja izpildes konteksts kodam pÄc gaidÄ«Å”anas nav svarÄ«gs. Tas ir arÄ« ieteikums no MS, rakstot kodu, kas tiks piegÄdÄts iesaiÅots bibliotÄkÄ.
PakavÄsimies nedaudz vairÄk pie tÄ, kÄ jÅ«s varat sagaidÄ«t uzdevuma pabeigÅ”anu. ZemÄk ir koda piemÄrs ar komentÄriem par to, kad gaidÄ«tais ir izpildÄ«ts nosacÄ«ti labi un kad tas ir izpildÄ«ts nosacÄ«ti slikti.
public static async void AnotherMethod() {
int result = await AsyncMethod(); // good
result = AsyncMethod().Result; // bad
AsyncMethod().Wait(); // bad
IEnumerable<Task> tasks = new Task[] {
AsyncMethod(), OtherAsyncMethod()
};
await Task.WhenAll(tasks); // good
await Task.WhenAny(tasks); // good
Task.WaitAll(tasks.ToArray()); // bad
}
PirmajÄ piemÄrÄ mÄs gaidÄm, lÄ«dz uzdevums tiks pabeigts, nebloÄ·Äjot izsaucoÅ”o pavedienu; mÄs atgriezÄ«simies pie rezultÄta apstrÄdes tikai tad, kad tas jau bÅ«s; lÄ«dz tam izsaucoÅ”ais pavediens tiek atstÄts savÄ ziÅÄ.
OtrajÄ variantÄ mÄs bloÄ·Äjam izsaucoÅ”o pavedienu, lÄ«dz tiek aprÄÄ·inÄts metodes rezultÄts. Tas ir slikti ne tikai tÄpÄc, ka esam aizÅÄmuÅ”i pavedienu, tik vÄrtÄ«gu programmas resursu, ar vienkÄrÅ”u dÄ«kstÄvi, bet arÄ« tÄpÄc, ka, ja izsauktÄs metodes kods satur gaidÄ«Å”anu un sinhronizÄcijas konteksts prasa atgriezties pie izsaucoÅ”Ä pavediena pÄc gaidiet, tad iegÅ«sim strupceļu : IzsaucoÅ”ais pavediens gaida, kad tiks aprÄÄ·inÄts asinhronÄs metodes rezultÄts, asinhronÄ metode veltÄ«gi mÄÄ£ina turpinÄt izpildi izsaucoÅ”ajÄ pavedienÄ.
VÄl viens Ŕīs pieejas trÅ«kums ir sarežģīta kļūdu apstrÄde. Fakts ir tÄds, ka kļūdas asinhronajÄ kodÄ, izmantojot async/await, ir ļoti viegli apstrÄdÄjamas - tÄs darbojas tÄpat kÄ tad, ja kods bÅ«tu sinhrons. Ja uzdevumam piemÄrojam sinhrono gaidÄ«Å”anas eksorcismu, sÄkotnÄjais izÅÄmums pÄrvÄrÅ”as par AggregateException, t.i. Lai apstrÄdÄtu izÅÄmumu, jums bÅ«s jÄpÄrbauda InnerException tips un paÅ”am jÄieraksta if Ä·Äde vienÄ nozvejas blokÄ vai jÄizmanto noÄ·ere, kad tiek konstruÄta, nevis C# pasaulÄ pazÄ«stamÄ uztverÅ”anas bloku Ä·Äde.
TreÅ”ais un pÄdÄjais piemÄri arÄ« ir atzÄ«mÄti kÄ slikti tÄ paÅ”a iemesla dÄļ un satur visas tÄs paÅ”as problÄmas.
Metodes WhenAny un WhenAll ir ļoti Ärtas, lai gaidÄ«tu uzdevumu grupu; tÄs apvieno uzdevumu grupu vienÄ, kas tiks aktivizÄta vai nu tad, kad pirmo reizi tiek aktivizÄts uzdevums no grupas, vai arÄ« tad, kad visi uzdevumi ir pabeiguÅ”i izpildi.
Apturot pavedienus
DažÄdu iemeslu dÄļ var bÅ«t nepiecieÅ”ams apturÄt plÅ«smu pÄc tÄs sÄkÅ”anÄs. Ir vairÄki veidi, kÄ to izdarÄ«t. Pavedienu klasei ir divas atbilstoÅ”i nosauktas metodes: PÄrtraukt Šø PÄrtraukumu. Pirmo ļoti neiesaka lietot, jo pÄc tÄ izsaukÅ”anas jebkurÄ nejauÅ”Ä brÄ«dÄ«, jebkuras instrukcijas apstrÄdes laikÄ tiks izmests izÅÄmums ThreadAbortedException. JÅ«s negaidÄt, ka Å”Äds izÅÄmums tiks izmests, palielinot jebkuru veselu mainÄ«go, vai ne? Un, izmantojot Å”o metodi, tÄ ir ļoti reÄla situÄcija. Ja jums ir nepiecieÅ”ams neļaut CLR Ä£enerÄt Å”Ädu izÅÄmumu noteiktÄ koda sadaļÄ, varat to iekļaut izsaukumos Thread.BeginCriticalRegion, Thread.EndCriticalRegion. JebkurÅ” kods, kas ierakstÄ«ts beigu blokÄ, tiek ietÄ«ts Å”Ädos izsaukumos. Å Ä« iemesla dÄļ ietvara koda dziļumos var atrast blokus ar tukÅ”u mÄÄ£inÄjumu, bet ne ar tukÅ”u galu. Microsoft tik ļoti attur Å”o metodi, ka viÅi to neiekļÄva .net kodolÄ.
PÄrtraukÅ”anas metode darbojas paredzamÄk. Tas var pÄrtraukt pavedienu ar izÅÄmumu ThreadInterruptedException tikai tajos brīžos, kad pavediens ir gaidÄ«Å”anas stÄvoklÄ«. Tas pÄriet Å”ajÄ stÄvoklÄ«, kad karÄjas, gaidot WaitHandle, bloÄ·ÄÅ”anu vai pÄc Thread.Sleep izsaukÅ”anas.
Abas iepriekÅ” aprakstÄ«tÄs iespÄjas ir sliktas to neparedzamÄ«bas dÄļ. RisinÄjums ir izmantot struktÅ«ru CancellationToken un klase CancellationTokenSource. Lieta ir Å”Äda: tiek izveidots CancellationTokenSource klases gadÄ«jums, un tikai tas, kuram tas pieder, var apturÄt darbÄ«bu, izsaucot metodi. Atcelt. PaÅ”ai darbÄ«bai tiek nodots tikai atcelÅ”anas marÄ·ieris. CancellationToken Ä«paÅ”nieki nevar paÅ”i atcelt darbÄ«bu, bet var tikai pÄrbaudÄ«t, vai darbÄ«ba ir atcelta. Å im nolÅ«kam ir BÅ«la rekvizÄ«ts IsCancelationRequested un metode ThrowIfCancelRequested. PÄdÄjais radÄ«s izÅÄmumu TaskCancelledException ja CancellationToken instancÄ tika izsaukta atcelÅ”anas metode. Un Å”o metodi es iesaku izmantot. Tas ir uzlabojums salÄ«dzinÄjumÄ ar iepriekÅ”ÄjÄm opcijÄm, iegÅ«stot pilnÄ«gu kontroli pÄr to, kurÄ brÄ«dÄ« izÅÄmuma darbÄ«bu var pÄrtraukt.
BrutÄlÄkÄ iespÄja pavediena apturÄÅ”anai ir izsaukt Win32 API funkciju TerminateThread. CLR darbÄ«ba pÄc Ŕīs funkcijas izsaukÅ”anas var bÅ«t neparedzama. MSDN par Å”o funkciju ir rakstÄ«ts: āTerminateThread ir bÄ«stama funkcija, kas jÄizmanto tikai ekstremÄlÄkajos gadÄ«jumos. "
MantotÄ API pÄrveidoÅ”ana par uzdevumu balstÄ«tu, izmantojot FromAsync metodi
Ja jums ir paveicies strÄdÄt pie projekta, kas tika uzsÄkts pÄc tam, kad tika ieviesti uzdevumi un vairs neradÄ«ja klusas Å”ausmas lielÄkajai daļai izstrÄdÄtÄju, jums nebÅ«s jÄrisina daudz vecu API ā gan treÅ”o puÅ”u, gan jÅ«su komandas. pagÄtnÄ ir spÄ«dzinÄjusi. Par laimi, .NET Framework komanda par mums parÅ«pÄjÄs, lai gan varbÅ«t mÄrÄ·is bija parÅ«pÄties par mums paÅ”iem. Lai kÄ arÄ« bÅ«tu, .NET ir vairÄki rÄ«ki, kas ļauj nesÄpÄ«gi pÄrveidot vecÄs asinhronÄs programmÄÅ”anas pieejÄs uzrakstÄ«to kodu uz jauno. Viena no tÄm ir TaskFactory metode FromAsync. TÄlÄk esoÅ”ajÄ koda piemÄrÄ es iesaiÅoju vecÄs WebRequest klases asinhronÄs metodes uzdevumÄ, izmantojot Å”o metodi.
Å is ir tikai piemÄrs, un maz ticams, ka tas bÅ«s jÄdara ar iebÅ«vÄtiem tipiem, taÄu jebkurÅ” vecs projekts ir vienkÄrÅ”i pilns ar BeginDoSomething metodÄm, kas atgriež IAsyncResult un EndDoSomething metodes, kas to saÅem.
PÄrveidojiet mantoto API uz uzdevumu balstÄ«tu, izmantojot klasi TaskCompletionSource
VÄl viens svarÄ«gs instruments, kas jÄÅem vÄrÄ, ir klase TaskCompletionSource. Funkciju, mÄrÄ·a un darbÄ«bas principa ziÅÄ tas var nedaudz atgÄdinÄt ThreadPool klases metodi RegisterWaitForSingleObject, par kuru rakstÄ«ju iepriekÅ”. Izmantojot Å”o klasi, programmÄ Tasks varat viegli un Ärti ietÄ«t vecÄs asinhronÄs API.
Teiksiet, ka es jau runÄju par Å”iem mÄrÄ·iem paredzÄto TaskFactory klases metodi FromAsync. Å eit mums bÅ«s jÄatceras visa .net asinhrono modeļu izstrÄdes vÄsture, ko Microsoft ir piedÄvÄjis pÄdÄjo 15 gadu laikÄ: pirms uzdevumiem balstÄ«tas asinhronÄs shÄmas (TAP) bija asinhronÄs programmÄÅ”anas modelis (APP), kas bija par metodÄm SÄktDoSomething atgriežas IAsyncResult un metodes beigasDoSomething, kas to pieÅem, un Å”o gadu mantojumam FromAsync metode ir vienkÄrÅ”i ideÄla, taÄu laika gaitÄ tÄ tika aizstÄta ar notikumiem balstÄ«tu asinhrono modeli (EAP), kurÄ tika pieÅemts, ka notikums tiks aktivizÄts, kad asinhronÄ darbÄ«ba tiks pabeigta.
TaskCompletionSource ir lieliski piemÄrots uzdevumu un mantoto API, kas veidotas ap notikumu modeli, iesaiÅoÅ”anai. TÄ darba bÅ«tÄ«ba ir Å”Äda: Ŕīs klases objektam ir Publisks rekvizÄ«ts tipa Task, kura stÄvokli var kontrolÄt, izmantojot klases TaskCompletionSource metodes SetResult, SetException u.c. VietÄs, kur Å”im uzdevumam tika lietots gaidÄ«Å”anas operators, tas tiks izpildÄ«ts vai neizdosies ar izÅÄmumu atkarÄ«bÄ no TaskCompletionSource izmantotÄs metodes. Ja tas joprojÄm nav skaidrs, apskatÄ«sim Å”o koda piemÄru, kur kÄds vecs EAP API ir ietÄ«ts uzdevumÄ, izmantojot TaskCompletionSource: kad notikums tiek aktivizÄts, uzdevums tiks pÄrsÅ«tÄ«ts uz stÄvokli Pabeigts un metodi, kas izmantoja gaidÄ«Å”anas operatoru. uz Å”o Uzdevumu atsÄks tÄ izpildi pÄc objekta saÅemÅ”anas radÄ«t.
public static Task<Result> DoAsync(this SomeApiInstance someApiObj) {
var completionSource = new TaskCompletionSource<Result>();
someApiObj.Done +=
result => completionSource.SetResult(result);
someApiObj.Do();
result completionSource.Task;
}
TaskCompletionSource padomi un triki
Veco API iesaiÅoÅ”ana nav viss, ko var paveikt, izmantojot TaskCompletionSource. Å Ä«s klases izmantoÅ”ana paver interesantu iespÄju izstrÄdÄt dažÄdus API uzdevumiem, kas neaizÅem pavedienus. Un straume, kÄ mÄs atceramies, ir dÄrgs resurss, un to skaits ir ierobežots (galvenokÄrt ar RAM apjomu). Å o ierobežojumu var viegli sasniegt, izstrÄdÄjot, piemÄram, ielÄdÄtu tÄ«mekļa lietojumprogrammu ar sarežģītu biznesa loÄ£iku. ApsvÄrsim iespÄjas, par kurÄm es runÄju, Ä«stenojot tÄdu triku kÄ Long-Polling.
ÄŖsÄk sakot, trika bÅ«tÄ«ba ir Å”Äda: jums ir jÄsaÅem informÄcija no API par dažiem notikumiem, kas notiek tÄs pusÄ, savukÄrt API kaut kÄdu iemeslu dÄļ nevar ziÅot par notikumu, bet var tikai atgriezt stÄvokli. KÄ piemÄru var minÄt visas API, kas izveidotas uz HTTP bÄzes pirms WebSocket laikiem vai kad kÄdu iemeslu dÄļ nebija iespÄjams izmantot Å”o tehnoloÄ£iju. Klients var jautÄt HTTP serverim. HTTP serveris pats nevar uzsÄkt saziÅu ar klientu. VienkÄrÅ”s risinÄjums ir aptaujÄt serveri, izmantojot taimeri, taÄu tas rada papildu slodzi serverim un papildu aizkavi vidÄji TimerInterval / 2. Lai to apietu, tika izgudrots triks ar nosaukumu Long Polling, kas ietver atbildes aizkavÄÅ”anu no plkst. serveri, lÄ«dz beidzas Taimauts vai notiks notikums. Ja notikums ir noticis, tad tas tiek apstrÄdÄts, ja nÄ, tad pieprasÄ«jums tiek nosÅ«tÄ«ts vÄlreiz.
TaÄu Å”Äds risinÄjums izrÄdÄ«sies Å”ausmÄ«gs, tiklÄ«dz pieaugs pasÄkumu gaidoÅ”o klientu skaits, jo... Katrs Å”Äds klients aizÅem veselu pavedienu, gaidot notikumu. JÄ, un mÄs saÅemam papildu 1 ms aizkavi, kad notikums tiek aktivizÄts, visbiežÄk tas nav bÅ«tiski, bet kÄpÄc programmatÅ«ru padarÄ«t sliktÄku, nekÄ tÄ var bÅ«t? Ja noÅemam Thread.Sleep(1), tad velti vienu procesora kodolu ielÄdÄsim 100% dÄ«kstÄvÄ, griežoties bezjÄdzÄ«gÄ ciklÄ. Izmantojot TaskCompletionSource, varat viegli pÄrveidot Å”o kodu un atrisinÄt visas iepriekÅ” norÄdÄ«tÄs problÄmas:
class LongPollingApi {
private Dictionary<int, TaskCompletionSource<Msg>> tasks;
public async Task<Msg> AcceptMessageAsync(int userId, int duration) {
var cs = new TaskCompletionSource<Msg>();
tasks[userId] = cs;
await Task.WhenAny(Task.Delay(duration), cs.Task);
return cs.Task.IsCompleted ? cs.Task.Result : null;
}
public void SendMessage(int userId, Msg m) {
if (tasks.TryGetValue(userId, out var completionSource))
completionSource.SetResult(m);
}
}
Å is kods nav gatavs ražoÅ”anai, bet tikai demonstrÄcija. Lai to izmantotu reÄlos gadÄ«jumos, jums ir arÄ« vismaz jÄrisina situÄcija, kad ziÅojums tiek saÅemts laikÄ, kad neviens to negaida: Å”ajÄ gadÄ«jumÄ AsseptMessageAsync metodei jÄatgriež jau pabeigts uzdevums. Ja tas ir visizplatÄ«tÄkais gadÄ«jums, varat padomÄt par ValueTask izmantoÅ”anu.
Kad mÄs saÅemam pieprasÄ«jumu pÄc ziÅojuma, mÄs izveidojam un ievietojam vÄrdnÄ«cÄ TaskCompletionSource un pÄc tam gaidÄm, kas notiks vispirms: beidzas norÄdÄ«tais laika intervÄls vai tiek saÅemts ziÅojums.
ValueTask: kÄpÄc un kÄ
Asinhronie/gaidÄ«Å”anas operatori, tÄpat kÄ ienesÄ«guma atgrieÅ”anas operators, no metodes Ä£enerÄ stÄvokļa maŔīnu, un tÄ ir jauna objekta izveide, kas gandrÄ«z vienmÄr nav svarÄ«ga, bet retos gadÄ«jumos var radÄ«t problÄmu. Å is gadÄ«jums var bÅ«t metode, kuru sauc patieÅ”Äm bieži, mÄs runÄjam par desmitiem un simtiem tÅ«kstoÅ”u zvanu sekundÄ. Ja Å”Äda metode ir uzrakstÄ«ta tÄ, ka vairumÄ gadÄ«jumu tÄ atgriež rezultÄtu, apejot visas gaidÄ«Å”anas metodes, tad .NET nodroÅ”ina rÄ«ku, lai to optimizÄtu - ValueTask struktÅ«ru. Lai tas bÅ«tu skaidrs, apskatÄ«sim tÄs izmantoÅ”anas piemÄru: ir keÅ”atmiÅa, kuru mÄs ļoti bieži apmeklÄjam. TajÄ ir dažas vÄrtÄ«bas, un tad mÄs tÄs vienkÄrÅ”i atdodam; ja nÄ, tad dodamies uz kÄdu lÄnu IO, lai tÄs iegÅ«tu. Es vÄlos to darÄ«t asinhroni, kas nozÄ«mÄ, ka visa metode izrÄdÄs asinhrona. TÄdÄjÄdi acÄ«mredzamais veids, kÄ rakstÄ«t metodi, ir Å”Äds:
public async Task<string> GetById(int id) {
if (cache.TryGetValue(id, out string val))
return val;
return await RequestById(id);
}
SakarÄ ar vÄlmi nedaudz optimizÄt un nedaudz baidoties no tÄ, ko Roslyn radÄ«s, apkopojot Å”o kodu, varat pÄrrakstÄ«t Å”o piemÄru Å”Ädi:
public Task<string> GetById(int id) {
if (cache.TryGetValue(id, out string val))
return Task.FromResult(val);
return RequestById(id);
}
PatieÅ”Äm, optimÄlais risinÄjums Å”ajÄ gadÄ«jumÄ bÅ«tu karstÄ ceļa optimizÄÅ”ana, proti, vÄrtÄ«bas iegÅ«Å”ana no vÄrdnÄ«cas bez liekiem pieŔķīrumiem un slodzes GC, savukÄrt tajos retos gadÄ«jumos, kad mums tomÄr jÄdodas uz IO pÄc datiem. , viss paliks plus / mÄ«nus vecajÄ veidÄ:
public ValueTask<string> GetById(int id) {
if (cache.TryGetValue(id, out string val))
return new ValueTask<string>(val);
return new ValueTask<string>(RequestById(id));
}
ApskatÄ«sim Å”o koda daļu tuvÄk: ja keÅ”atmiÅÄ ir vÄrtÄ«ba, mÄs izveidojam struktÅ«ru, pretÄjÄ gadÄ«jumÄ reÄlais uzdevums tiks ietÄ«ts jÄgpilnÄ. IzsaucoÅ”ajam kodam ir vienalga, kurÄ ceÄ¼Ä Å”is kods tika izpildÄ«ts: ValueTask no C# sintakses viedokļa Å”ajÄ gadÄ«jumÄ darbosies tÄpat kÄ parastais uzdevums.
TaskSchedulers: uzdevumu palaiÅ”anas stratÄÄ£iju pÄrvaldÄ«ba
NÄkamÄ API, ko es vÄlÄtos apsvÄrt, ir klase Uzdevumu plÄnotÄjs un tÄ atvasinÄjumi. Es jau minÄju iepriekÅ”, ka TPL ir iespÄja pÄrvaldÄ«t stratÄÄ£ijas uzdevumu izplatÄ«Å”anai pa pavedieniem. Å Ädas stratÄÄ£ijas ir definÄtas klases TaskScheduler pÄcnÄcÄjos. BibliotÄkÄ var atrast gandrÄ«z jebkuru stratÄÄ£iju, kas jums varÄtu bÅ«t nepiecieÅ”ama. ParallelExtensionsExtras, ko izstrÄdÄjis Microsoft, bet tas nav daļa no .NET, bet tiek piegÄdÄts kÄ Nuget pakotne. ÄŖsi apskatÄ«sim dažus no tiem:
CurrentThreadTaskScheduler ā izpilda uzdevumus paÅ”reizÄjÄ pavedienÄ
LimitedConcurrencyLevelTaskScheduler ā ierobežo vienlaicÄ«gi izpildÄmo uzdevumu skaitu ar parametru N, kas tiek pieÅemts konstruktorÄ
OrderedTaskScheduler ā ir definÄts kÄ LimitedConcurrencyLevelTaskScheduler(1), tÄpÄc uzdevumi tiks izpildÄ«ti secÄ«gi.
WorkStealingTaskScheduler - darbarÄ«ki darba zagÅ”ana pieeja uzdevumu sadalei. BÅ«tÄ«bÄ tas ir atseviŔķs ThreadPool. Atrisina problÄmu, ka .NET ThreadPool ir statiska klase, viena visÄm lietojumprogrammÄm, kas nozÄ«mÄ, ka tÄs pÄrslodze vai nepareiza izmantoÅ”ana vienÄ programmas daÄ¼Ä var izraisÄ«t blakusparÄdÄ«bas citÄ. TurklÄt ir ÄrkÄrtÄ«gi grÅ«ti saprast Å”Ädu defektu cÄloni. Tas. Var bÅ«t nepiecieÅ”ams izmantot atseviŔķus WorkStealingTaskSchedulers programmas daļÄs, kur ThreadPool izmantoÅ”ana var bÅ«t agresÄ«va un neparedzama.
QueuedTaskScheduler ā ļauj veikt uzdevumus atbilstoÅ”i prioritÄtes rindas noteikumiem
ThreadPerTaskScheduler ā izveido atseviŔķu pavedienu katram uzdevumam, kas tajÄ tiek izpildÄ«ts. Var bÅ«t noderÄ«gi uzdevumiem, kuru izpildei nepiecieÅ”ams neparedzami ilgs laiks.
Ir labs detalizÄts raksts par TaskSchedulers Microsoft emuÄrÄ.
Lai Ärti atkļūdotu visu, kas saistÄ«ts ar uzdevumiem, Visual Studio ir uzdevumu logs. Å ajÄ logÄ varat redzÄt paÅ”reizÄjo uzdevuma stÄvokli un pÄriet uz paÅ”laik izpildÄmo koda rindu.
PLinq un paralÄlÄ klase
Papildus uzdevumiem un visam, kas par tiem teikts, .NET ir vÄl divi interesanti rÄ«ki: PLinq (Linq2Parallel) un Parallel klase. Pirmais sola visu Linq darbÄ«bu paralÄlu izpildi vairÄkos pavedienos. Pavedienu skaitu var konfigurÄt, izmantojot paplaÅ”inÄÅ”anas metodi WithDegreeOfParallelism. DiemžÄl visbiežÄk PLinq noklusÄjuma režīmÄ nav pietiekami daudz informÄcijas par jÅ«su datu avota iekÅ”Äjiem elementiem, lai nodroÅ”inÄtu ievÄrojamu Ätruma pieaugumu, no otras puses, mÄÄ£inÄjuma izmaksas ir ļoti zemas: pirms tam jums vienkÄrÅ”i jÄizsauc AsParallel metode. Linq metožu Ä·Ädi un veikt veiktspÄjas testus. TurklÄt ir iespÄjams nodot papildu informÄciju PLinq par jÅ«su datu avota bÅ«tÄ«bu, izmantojot nodalÄ«jumu mehÄnismu. JÅ«s varat lasÄ«t vairÄk Å”eit Šø Å”eit.
ParalÄlÄ statiskÄ klase nodroÅ”ina metodes paralÄlai Foreach kolekcijas atkÄrtoÅ”anai, For cilpas izpildei un vairÄku delegÄtu izpildei paralÄli Invoke. PaÅ”reizÄjÄ pavediena izpilde tiks apturÄta lÄ«dz aprÄÄ·inu pabeigÅ”anai. Pavedienu skaitu var konfigurÄt, kÄ pÄdÄjo argumentu nododot ParallelOptions. Varat arÄ« norÄdÄ«t TaskScheduler un CancellationToken, izmantojot opcijas.
Atzinumi
Kad sÄku rakstÄ«t Å”o rakstu, balstoties uz sava referÄta materiÄliem un informÄciju, ko savÄcu darba laikÄ pÄc tÄ, nebiju gaidÄ«jis, ka tÄ bÅ«s tik daudz. Tagad, kad teksta redaktors, kurÄ es rakstu Å”o rakstu, man pÄrmetoÅ”i paziÅo, ka 15. lappuse ir pagÄjusi, es apkopoÅ”u starprezultÄtus. Citi triki, API, vizuÄlie rÄ«ki un kļūmes tiks aplÅ«kotas nÄkamajÄ rakstÄ.
SecinÄjumi:
Lai izmantotu mÅ«sdienu datoru resursus, ir jÄzina rÄ«ki darbam ar pavedieniem, asinhronija un paralÄlisms.
.NET Å”iem nolÅ«kiem ir daudz dažÄdu rÄ«ku
Ne visi no tiem parÄdÄ«jÄs uzreiz, tÄpÄc jÅ«s bieži varat atrast mantotos, tomÄr ir veidi, kÄ pÄrvÄrst vecÄs API bez Ä«paÅ”as piepÅ«les.
Darbu ar pavedieniem .NET attÄlo Thread un ThreadPool klases
Metodes Thread.Abort, Thread.Interrupt un Win32 API TerminateThread ir bÄ«stamas un nav ieteicamas lietoÅ”anai. TÄ vietÄ labÄk ir izmantot CancellationToken mehÄnismu
PlÅ«sma ir vÄrtÄ«gs resurss, un tÄ piedÄvÄjums ir ierobežots. JÄizvairÄs no situÄcijÄm, kad pavedieni ir aizÅemti, gaidot notikumus. Å im nolÅ«kam ir Ärti izmantot klasi TaskCompletionSource
VisjaudÄ«gÄkie un modernÄkie .NET rÄ«ki darbam ar paralÄlismu un asinhroniju ir Uzdevumi.
Varat kontrolÄt uzdevumu sadalÄ«jumu pa pavedieniem, izmantojot no TaskScheduler atvasinÄtas klases
ValueTask struktÅ«ra var bÅ«t noderÄ«ga karsto ceļu un atmiÅas trafika optimizÄÅ”anai
Visual Studio uzdevumu un pavedienu logi sniedz daudz informÄcijas, kas noderÄ«ga vairÄku pavedienu vai asinhrona koda atkļūdoÅ”anai
PLinq ir lielisks rÄ«ks, taÄu tam var nebÅ«t pietiekami daudz informÄcijas par jÅ«su datu avotu, taÄu to var novÄrst, izmantojot sadalÄ«Å”anas mehÄnismu