.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.

.NET: rīki darbam ar daudzpavedienu un asinhroniju. 1. daļa

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.

object state = null;
WebRequest wr = WebRequest.CreateHttp("http://github.com");
await Task.Factory.FromAsync(
    wr.BeginGetResponse,
    we.EndGetResponse
);

Å 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.

while(!eventOccures && !timeoutExceeded)  {

  CheckTimout();
  CheckEvent();
  Thread.Sleep(1);
}

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.

.NET: rīki darbam ar daudzpavedienu un asinhroniju. 1. daļa

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.
  • C# async/await operatori ievieÅ” nebloķējoÅ”as gaidÄ«Å”anas koncepciju
  • 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
  • Lai varētu turpināt ...

Avots: www.habr.com

Pievieno komentāru