.NET: įrankiai, skirti dirbti su kelių gijų ir asinchronija. 1 dalis

Skelbiu originalų straipsnį apie Habr, kurio vertimas paskelbtas korporacijoje dienoraščio įrašas.

Poreikis kažką daryti asinchroniškai, nelaukiant rezultato čia ir dabar, arba didelius darbus paskirstyti keliems jį atliekantiems padaliniams, egzistavo dar iki kompiuterių atsiradimo. Su jų atsiradimu šis poreikis tapo labai apčiuopiamas. Dabar, 2019 m., rašau šį straipsnį nešiojamajame kompiuteryje su 8 branduolių „Intel Core“ procesoriumi, kuriame lygiagrečiai veikia daugiau nei šimtas procesų ir dar daugiau gijų. Šalia stovi šiek tiek aptriušęs telefonas, pirktas prieš porą metų, jame yra 8 branduolių procesorius. Teminiuose šaltiniuose gausu straipsnių ir vaizdo įrašų, kuriuose jų autoriai žavisi šių metų flagmanais išmaniaisiais telefonais, kuriuose yra 16 branduolių procesoriai. MS Azure suteikia virtualią mašiną su 20 branduolių procesoriumi ir 128 TB RAM už mažiau nei 2 USD per valandą. Deja, neįmanoma išgauti maksimumo ir panaudoti šią galią nesugebėjus valdyti gijų sąveikos.

terminologija

Procesas - OS objektas, izoliuota adresų erdvė, yra gijų.
Siūlas - OS objektas, mažiausias vykdymo vienetas, proceso dalis, gijos dalijasi atmintimi ir kitais ištekliais procese.
Daugiafunkcinis darbas - OS savybė, galimybė vienu metu vykdyti kelis procesus
Daugiagyslis - procesoriaus savybė, galimybė duomenų apdorojimui naudoti kelis branduolius
Daugiafunkcis apdorojimas - kompiuterio savybė, galimybė vienu metu fiziškai dirbti su keliais procesoriais
Daugiagija - proceso savybė, galimybė paskirstyti duomenų apdorojimą tarp kelių gijų.
Lygiagretumas - fiziškai vienu metu atliekant kelis veiksmus per laiko vienetą
Asinchronija - operacijos vykdymas nelaukiant, kol šis apdorojimas bus baigtas; vykdymo rezultatas gali būti apdorotas vėliau.

Metafora

Ne visi apibrėžimai yra geri ir kai kuriems reikia papildomo paaiškinimo, todėl prie oficialiai įvestos terminijos pridėsiu metaforą apie pusryčių gaminimą. Pagal šią metaforą pusryčių gaminimas yra procesas.

Ruošdamas pusryčius ryte aš (CPU) Aš ateinu į virtuvę (Kompiuteris). Turiu 2 rankas (Ritiniai). Virtuvėje yra daug prietaisų (IO): orkaitė, virdulys, skrudintuvas, šaldytuvas. Įjungiu dujas, dedu ant jos keptuvę ir įpilu aliejaus nelaukdamas kol įkais (asinchroniškai, Neblokuojantis-IO-laukti), išimu iš šaldytuvo kiaušinius ir sudaužau į lėkštę, tada plaku viena ranka (Siūlas Nr.1), ir antra (Siūlas Nr.2) laikydami lėkštę (bendras išteklius). Dabar norėčiau įjungti virdulį, bet man neužtenka rankų (Siūlas Badavimas) Per tą laiką įkaista keptuvė (Apdorojamas rezultatas), į kurią supilu tai, ką išplakiau. Prieinu prie virdulio, įjungiu jį ir kvailai žiūriu, kaip jame verda vanduo (Blokavimas-IO-laukti), nors per tą laiką jis galėjo išplauti lėkštę, kurioje plakė omletą.

Omletą viriau tik 2 rankomis, o daugiau neturiu, bet tuo pačiu metu omleto plakimo momentu iš karto vyko 3 operacijos: omleto plakimas, lėkštės laikymas, keptuvės įkaitinimas. Centrinis procesorius yra greičiausia kompiuterio dalis, IO yra tai, kas dažniausiai sulėtėja, todėl dažnai efektyvus sprendimas yra kažkuo užimti procesorių gaunant duomenis iš IO.

Tęsiant metaforą:

  • Jei omleto ruošimo procese taip pat bandyčiau persirengti, tai būtų multitaskingo pavyzdys. Svarbus niuansas: kompiuteriai yra daug geresni už žmones.
  • Virtuvė su keliais šefais, pavyzdžiui restorane – kelių branduolių kompiuteris.
  • Daug restoranų maisto aikštelėje prekybos centre – duomenų centre

.NET įrankiai

.NET gerai dirba su gijomis, kaip ir su daugeliu kitų dalykų. Su kiekviena nauja versija ji pristato vis daugiau naujų darbo su jais įrankių, naujų abstrakcijos sluoksnių per OS gijas. Dirbdami su abstrakcijų konstravimu, sistemos kūrėjai naudoja metodą, kuris palieka galimybę, naudojant aukšto lygio abstrakciją, nusileisti vienu ar keliais lygiais žemiau. Dažniausiai tai nėra būtina, iš tikrųjų tai atveria duris šaudyti sau į koją iš kulkosvaidžio, tačiau kartais retais atvejais tai gali būti vienintelis būdas išspręsti problemą, kuri nėra išspręsta esant dabartiniam abstrakcijos lygiui. .

Įrankiais turiu galvoje tiek programų programavimo sąsajas (API), kurias teikia sistema, tiek trečiųjų šalių paketai, taip pat ištisus programinės įrangos sprendimus, kurie supaprastina bet kokių problemų, susijusių su kelių gijų kodu, paiešką.

Pradedant giją

„Tread“ klasė yra pati paprasčiausia .NET klasė, skirta darbui su gijomis. Konstruktorius priima vieną iš dviejų delegatų:

  • ThreadStart – nėra parametrų
  • ParametrizedThreadStart – su vienu objekto tipo parametru.

Delegacija bus vykdoma naujai sukurtoje gijoje, iškvietus metodą Start.Jei konstruktoriui buvo perduotas ParametrizedThreadStart tipo delegatas, tada į Start metodą turi būti perduotas objektas. Šis mechanizmas reikalingas bet kokiai vietinei informacijai perkelti į srautą. Verta paminėti, kad gijos kūrimas yra brangi operacija, o pati gija yra sunkus objektas, bent jau dėl to, kad ji skiria 1 MB atminties krūvoje ir reikalauja sąveikos su OS API.

new Thread(...).Start(...);

ThreadPool klasė atspindi baseino koncepciją. NET versijoje gijų telkinys yra inžinerijos kūrinys, o „Microsoft“ kūrėjai įdėjo daug pastangų siekdami užtikrinti, kad jis optimaliai veiktų įvairiuose scenarijuose.

Bendra koncepcija:

Nuo programos paleidimo momento ji sukuria keletą rezervuotų gijų fone ir suteikia galimybę jas naudoti. Jei siūlai naudojami dažnai ir daug, grupė plečiasi, kad atitiktų skambinančiojo poreikius. Kai tinkamu metu telkinyje nėra laisvų gijų, jis arba lauks, kol grįš viena iš gijų, arba sukurs naują. Iš to išplaukia, kad gijų telkinys puikiai tinka kai kuriems trumpalaikiams veiksmams ir prastai tinka operacijoms, kurios veikia kaip paslaugos per visą programos veikimą.

Norint naudoti giją iš telkinio, yra metodas „QueueUserWorkItem“, kuris priima WaitCallback tipo delegatą, turintį tą patį parašą kaip ir ParametrizedThreadStart, o jam perduotas parametras atlieka tą pačią funkciją.

ThreadPool.QueueUserWorkItem(...);

Mažiau žinomas gijų telkinio metodas RegisterWaitForSingleObject naudojamas neblokuojančioms IO operacijoms organizuoti. Į šį metodą perduotas atstovas bus iškviestas, kai metodui perduota „WaitHandle“ bus „Atleista“.

ThreadPool.RegisterWaitForSingleObject(...)

.NET turi gijos laikmatį ir skiriasi nuo WinForms/WPF laikmačių tuo, kad jo tvarkytojas bus iškviestas gijoje, paimtoje iš telkinio.

System.Threading.Timer

Taip pat yra gana egzotiškas būdas išsiųsti delegatą vykdyti į giją iš telkinio - metodas BeginInvoke.

DelegateInstance.BeginInvoke

Norėčiau trumpai pasilikti ties funkcija, kuriai galima pavadinti daugelį aukščiau išvardintų metodų – CreateThread iš Kernel32.dll Win32 API. Dėl išorinių metodų mechanizmo yra būdas iškviesti šią funkciją. Tokį raginimą mačiau tik vieną kartą siaubingame palikimo kodo pavyzdyje, o autoriaus, kuris būtent tai padarė, motyvacija man vis dar lieka paslaptimi.

Kernel32.dll CreateThread

Gijų peržiūra ir derinimas

Jūsų sukurtas gijas, visus trečiųjų šalių komponentus ir .NET telkinį galima peržiūrėti „Visual Studio“ gijų lange. Šiame lange bus rodoma gijos informacija tik tada, kai programa derinama ir veikia pertraukos režimu. Čia galite patogiai peržiūrėti kiekvienos gijos krūvos pavadinimus ir prioritetus bei perjungti derinimą į konkrečią giją. Naudodami Thread klasės savybę Priority, galite nustatyti gijos prioritetą, kurį OC ir CLR suvoks kaip rekomendaciją dalindami procesoriaus laiką tarp gijų.

.NET: įrankiai, skirti dirbti su kelių gijų ir asinchronija. 1 dalis

Užduočių lygiagreti biblioteka

Task Parallel Library (TPL) buvo pristatyta .NET 4.0. Dabar tai yra standartas ir pagrindinis įrankis dirbant su asinchronija. Bet koks kodas, kuriame naudojamas senesnis metodas, laikomas senu. Pagrindinis TPL vienetas yra užduočių klasė iš System.Threading.Tasks vardų erdvės. Užduotis yra abstrakcija per giją. Su nauja C# kalbos versija gavome elegantišką būdą dirbti su užduotimis – asinchronizuoti/laukti operatorius. Šios sąvokos leido rašyti asinchroninį kodą taip, tarsi jis būtų paprastas ir sinchroniškas, tai leido net žmonėms, mažai suprantantiems vidinį gijų veikimą, rašyti jas naudojančias programas, kurios neužstringa atliekant ilgas operacijas. Asinchronizavimo / laukimo naudojimas yra vieno ar net kelių straipsnių tema, bet aš pabandysiu suprasti jos esmę keliais sakiniais:

  • async yra metodo, grąžinančio užduotį arba galią, modifikatorius
  • ir laukti yra neblokuojantis Užduotis laukia operatorius.

Dar kartą: operatorius await bendru atveju (yra išimčių) paleis esamą vykdymo giją toliau, o kai užduotis baigs vykdyti, ir giją (tiesą sakant, teisingiau būtų sakyti kontekstą , bet daugiau apie tai vėliau) ir toliau vykdys metodą. NET viduje šis mechanizmas įgyvendinamas taip pat, kaip ir pelningumo grąža, kai parašytas metodas virsta visa klase, kuri yra būsenos mašina ir gali būti vykdoma atskirais gabalais, priklausomai nuo šių būsenų. Visi norintys gali parašyti bet kokį paprastą kodą naudodami asynс/await, kompiliuoti ir peržiūrėti rinkinį naudodami JetBrains dotPeek su įjungtu kompiliatoriaus generuojamu kodu.

Pažvelkime į užduoties paleidimo ir naudojimo parinktis. Toliau pateiktame kodo pavyzdyje sukuriame naują užduotį, kuri neduoda nieko naudingo (Thread.Sleep (10000)), tačiau realiame gyvenime tai turėtų būti sudėtingas procesoriaus reikalaujantis darbas.

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
}

Užduotis sukuriama su keliomis parinktimis:

  • „LongRunning“ yra užuomina, kad užduotis nebus atlikta greitai, o tai reiškia, kad verta neimti gijos iš telkinio, o sukurti šiai užduočiai atskirą, kad nepakenktumėte kitiems.
  • AttachedToParent – ​​užduotys gali būti išdėstytos hierarchija. Jei ši parinktis buvo naudojama, tada užduotis gali būti tokioje būsenoje, kurioje ji pati buvo baigta ir laukia savo vaikų įvykdymo.
  • PreferFairness – reiškia, kad užduotis, išsiųstas vykdyti, būtų geriau įvykdyti anksčiau, o ne vėliau išsiųstas. Bet tai tik rekomendacija, o rezultatas nėra garantuotas.

Antrasis metodui perduotas parametras yra CancellationToken. Norint tinkamai tvarkyti operacijos atšaukimą jai prasidėjus, vykdomas kodas turi būti užpildytas CancellationToken būsenos patikrinimais. Jei patikrinimų nėra, atšaukimo metodas, iškviestas CancellationTokenSource objekte, galės sustabdyti užduoties vykdymą tik prieš jai prasidėjus.

Paskutinis parametras yra TaskScheduler tipo planavimo objektas. Ši klasė ir jos palikuonys yra skirti valdyti užduočių paskirstymo gijose strategijas; pagal numatytuosius nustatymus užduotis bus vykdoma atsitiktine gija iš telkinio.

Sukurtai užduočiai taikomas operatorius await, o tai reiškia, kad po jos parašytas kodas, jei toks yra, bus vykdomas tame pačiame kontekste (dažnai tai reiškia toje pačioje gijoje), kaip ir kodas prieš laukti.

Metodas pažymėtas kaip asinchroninis negaliojantis, o tai reiškia, kad jis gali naudoti laukimo operatorių, tačiau skambinimo kodas negalės laukti, kol bus įvykdytas. Jei tokia funkcija reikalinga, metodas turi grąžinti Užduotį. Metodai, pažymėti kaip asinchroninė galia, yra gana dažni: paprastai tai yra įvykių tvarkytojai ar kiti metodai, kurie veikia ugnies ir pamiršimo principu. Jei jums reikia ne tik suteikti galimybę palaukti iki vykdymo pabaigos, bet ir grąžinti rezultatą, tuomet turite naudoti Užduotis.

Užduotyje, kurią grąžino metodas „StartNew“, taip pat bet kurioje kitoje, galite iškviesti „ConfigureAwait“ metodą naudodami klaidingą parametrą, tada vykdymas po laukimo bus tęsiamas ne užfiksuotame kontekste, o savavališkame kontekste. Tai turėtų būti daroma visada, kai vykdymo kontekstas nėra svarbus kodui po laukimo. Tai taip pat yra MS rekomendacija rašant kodą, kuris bus pristatytas supakuotas bibliotekoje.

Pakalbėkime šiek tiek daugiau apie tai, kaip galite laukti, kol bus atlikta užduotis. Žemiau pateikiamas kodo pavyzdys su komentarais, kada lūkesčiai įvykdyti sąlyginai gerai, o kada sąlyginai prastai.

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
}

Pirmajame pavyzdyje laukiame, kol užduotis bus baigta, neužblokuodami iškviečiančios gijos; prie rezultato apdorojimo grįšime tik tada, kai jis jau yra; iki tol skambinanti gija paliekama savieigai.

Antruoju variantu blokuojame skambinimo giją, kol bus apskaičiuotas metodo rezultatas. Tai blogai ne tik dėl to, kad užėmėme giją, tokį vertingą programos šaltinį, paprasčiausiai nenaudodami, bet ir todėl, kad jei iškviečiamo metodo kodas laukia, o sinchronizavimo kontekstas reikalauja grįžti į iškvietimo giją po laukti, tada gausime aklavietę : Iškviečianti gija laukia, kol bus apskaičiuotas asinchroninio metodo rezultatas, asinchroninis metodas bergždžiai bando tęsti savo vykdymą iškvietimo gijoje.

Kitas šio metodo trūkumas yra sudėtingas klaidų apdorojimas. Faktas yra tas, kad asinchroninio kodo klaidas naudojant async/await labai lengva tvarkyti – jos elgiasi taip pat, lyg kodas būtų sinchroninis. Tuo tarpu jei užduočiai pritaikome sinchroninio laukimo egzorcizmą, pirminė išimtis virsta AggregateException, t.y. Norėdami apdoroti išimtį, turėsite išnagrinėti InnerException tipą ir pačiam parašyti if grandinę viename gaudymo bloke arba konstruodami naudoti gaudymą, o ne C# pasaulyje labiau žinomą gaudymo blokų grandinę.

Trečiasis ir paskutinis pavyzdžiai taip pat pažymėti kaip blogi dėl tos pačios priežasties ir juose yra tos pačios problemos.

„WhenAny“ ir „WhenAll“ metodai yra ypač patogūs laukiant užduočių grupės; jie sujungia užduočių grupę į vieną, kuri suaktyvinama pirmą kartą suaktyvinus grupės užduotį arba kai visos jos baigs vykdyti.

Siūlų stabdymas

Dėl įvairių priežasčių gali prireikti sustabdyti srautą jam prasidėjus. Yra keletas būdų tai padaryti. Thread klasėje yra du tinkamai pavadinti metodai: Nutraukti и Nutraukti. Pirmojo labai nerekomenduojama naudoti, nes jį iškvietus bet kuriuo atsitiktiniu momentu, bet kokios instrukcijos apdorojimo metu, bus išmesta išimtis ThreadAbortedException. Jūs nesitikite, kad tokia išimtis bus išmesta didinant bet kurį sveikąjį kintamąjį, tiesa? Ir naudojant šį metodą, tai yra labai reali situacija. Jei reikia neleisti, kad CLR sukurtų tokią išimtį tam tikroje kodo dalyje, galite ją įtraukti į skambučius Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Bet koks kodas, parašytas galutiniame bloke, yra suvyniotas į tokius skambučius. Dėl šios priežasties pagrindinio kodo gilumoje galite rasti blokų su tuščiu bandymu, bet ne tuščiu galu. „Microsoft“ taip neskatina šio metodo, kad neįtraukė jo į „.net core“.

Pertraukimo metodas veikia labiau nuspėjamai. Jis gali nutraukti giją su išimtimi ThreadInterruptedException tik tais momentais, kai siūlas yra laukimo būsenoje. Į šią būseną jis patenka kabodamas, kol laukia WaitHandle, užrakto arba iškviečiamas Thread.Sleep.

Abu aukščiau aprašyti variantai yra blogi dėl jų nenuspėjamumo. Sprendimas yra naudoti struktūrą CancellationToken ir klasė CancellationTokenSource. Esmė tokia: sukuriamas CancellationTokenSource klasės egzempliorius ir tik tas, kuriam jis priklauso, gali sustabdyti operaciją iškviesdamas metodą atšaukti. Pačiai operacijai perduodamas tik atšaukimo prieigos raktas. CancellationToken savininkai negali patys atšaukti operacijos, bet gali tik patikrinti, ar operacija buvo atšaukta. Tam yra Būlio ypatybė IsCancelationRequested ir metodas ThrowIfCancelRequested. Pastarasis padarys išimtį TaskCancelledException jei atšaukimo metodas buvo iškviestas CancellationToken egzemplioriuje. Ir aš rekomenduoju naudoti šį metodą. Tai yra patobulinimas, palyginti su ankstesnėmis parinktimis, nes visiškai kontroliuojama, kada išimties operacija gali būti nutraukta.

Žiauriausias gijos sustabdymo variantas yra iškviesti Win32 API funkciją TerminateThread. CLR elgesys iškvietus šią funkciją gali būti nenuspėjamas. MSDN apie šią funkciją parašyta: „TerminateThread yra pavojinga funkcija, kurią reikėtų naudoti tik kraštutiniais atvejais. “

Pasenusios API konvertavimas į užduočių pagrįstą naudojant FromAsync metodą

Jei jums pasisekė dirbti su projektu, kuris buvo pradėtas po to, kai buvo pristatytos užduotys ir nustojo kelti tylų siaubą daugumai kūrėjų, jums nereikės susidurti su daugybe senų API – tiek trečiųjų šalių, tiek jūsų komandos. kankino praeityje. Laimei, .NET Framework komanda mumis pasirūpino, nors galbūt tikslas buvo pasirūpinti savimi. Kad ir kaip būtų, .NET turi daugybę įrankių, leidžiančių neskausmingai konvertuoti senais asinchroninio programavimo būdais parašytą kodą į naująjį. Vienas iš jų yra „TaskFactory“ metodas „FromAsync“. Žemiau esančiame kodo pavyzdyje, naudodamas šį metodą, senus „WebRequest“ klasės asinchronizavimo metodus įtraukiu į užduotį.

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

Tai tik pavyzdys ir vargu ar jums teks tai daryti naudojant įtaisytuosius tipus, tačiau bet kuriame sename projekte tiesiog gausu BeginDoSomething metodų, kurie grąžina IAsyncResult ir EndDoSomething metodus, kurie juos gauna.

Konvertuokite seną API į užduočių pagrįstą naudodami klasę TaskCompletionSource

Kitas svarbus įrankis, į kurį reikia atsižvelgti, yra klasė TaskCompletionSource. Pagal funkcijas, paskirtį ir veikimo principą jis gali šiek tiek priminti ThreadPool klasės metodą RegisterWaitForSingleObject, apie kurį rašiau aukščiau. Naudodami šią klasę galite lengvai ir patogiai apvynioti senas asinchronines API į Tasks.

Sakysite, kad jau kalbėjau apie šiems tikslams skirtą TaskFactory klasės metodą FromAsync. Čia turėsime prisiminti visą .net asinchroninių modelių kūrimo istoriją, kurią Microsoft pasiūlė per pastaruosius 15 metų: prieš užduočių pagrindu sukurtą asinchroninį šabloną (TAP) egzistavo asinchroninio programavimo šablonas (APP), kuris buvo apie metodus PradėtiDoSomething grįžta IAsyncResult ir metodai Galas„DoSomething“, kuris jį priima, ir šių metų palikimui „FromAsync“ metodas yra tiesiog tobulas, tačiau laikui bėgant jis buvo pakeistas įvykiais pagrįstu asinchroniniu modeliu (IR AP), kuriame buvo daroma prielaida, kad įvykis bus paskelbtas, kai asinchroninė operacija bus baigta.

„TaskCompletionSource“ puikiai tinka užduotims ir senoms API, sukurtoms pagal įvykio modelį, apvynioti. Jo darbo esmė tokia: šios klasės objektas turi viešąją Task tipo ypatybę, kurios būseną galima valdyti naudojant TaskCompletionSource klasės SetResult, SetException ir kt. metodus. Vietose, kur šiai užduočiai buvo pritaikytas laukimo operatorius, ji bus vykdoma arba nepavyks, išskyrus išimtį, priklausomai nuo metodo, taikomo TaskCompletionSource. Jei vis dar neaišku, pažiūrėkime į šį kodo pavyzdį, kur kai kurios senos EAP API yra įtrauktos į užduotį naudojant TaskCompletionSource: kai įvykis suaktyvinamas, užduotis bus perkelta į būseną Baigta, o metodas, kuris pritaikė laukimo operatorių. į šią Užduotį atnaujins jos vykdymą, gavęs objektą rezultatas.

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“ patarimai ir gudrybės

Senų API įvyniojimas nėra viskas, ką galima padaryti naudojant TaskCompletionSource. Naudojant šią klasę, atsiveria įdomi galimybė kurti įvairias užduočių API, kurios nėra užimtos gijomis. O srautas, kaip prisimename, yra brangus išteklius ir jų skaičius ribojamas (daugiausia RAM kiekiu). Šį apribojimą galima lengvai pasiekti kuriant, pavyzdžiui, įkeltą žiniatinklio programą su sudėtinga verslo logika. Apsvarstykime galimybes, apie kurias kalbu, įgyvendinant tokį triuką kaip „Long-Polling“.

Trumpai tariant, triuko esmė tokia: reikia gauti informaciją iš API apie kai kuriuos jos pusėje įvykusius įvykius, tuo tarpu API kažkodėl negali pranešti apie įvykį, o gali tik grąžinti būseną. To pavyzdys yra visos API, sukurtos ant HTTP, iki WebSocket laikų arba kai dėl kokių nors priežasčių buvo neįmanoma naudoti šios technologijos. Klientas gali paklausti HTTP serverio. HTTP serveris pats negali pradėti ryšio su klientu. Paprastas sprendimas yra apklausa serverį naudojant laikmatį, tačiau tai sukuria papildomą serverio apkrovą ir papildomą delsą vidutiniškai TimerInterval / 2. Norėdami tai apeiti, buvo išrastas triukas, vadinamas Long Polling, kuris apima atsakymo atidėjimą iš serveryje, kol pasibaigs laikas arba įvyks įvykis. Jei įvykis įvyko, jis apdorojamas, jei ne, tada užklausa siunčiama dar kartą.

while(!eventOccures && !timeoutExceeded)  {

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

Tačiau toks sprendimas pasirodys baisus, kai tik padaugės renginio laukiančių klientų, nes... Kiekvienas toks klientas užima visą giją, laukiančią įvykio. Taip, ir mes gauname papildomą 1 ms delsą, kai įvykis suaktyvinamas, dažniausiai tai nėra reikšminga, bet kam daryti programinę įrangą blogesnę nei ji gali būti? Jei pašalinsime Thread.Sleep(1), tai veltui vieną procesoriaus branduolį įkelsime 100% tuščiąja eiga, besisukantį nenaudingu ciklu. Naudodami TaskCompletionSource galite lengvai perdaryti šį kodą ir išspręsti visas aukščiau nurodytas problemas:

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 kodas nėra paruoštas gamybai, o tik demonstracinis. Norint jį naudoti realiais atvejais, taip pat reikia bent jau susidoroti su situacija, kai pranešimas ateina tuo metu, kai niekas jo nesitiki: tokiu atveju AsseptMessageAsync metodas turėtų grąžinti jau atliktą užduotį. Jei tai yra labiausiai paplitęs atvejis, galite pagalvoti apie „ValueTask“ naudojimą.

Kai gauname užklausą dėl pranešimo, sukuriame ir į žodyną įdedame TaskCompletionSource, o tada laukiame, kas nutiks pirmiausia: pasibaigs nurodytas laiko intervalas ar bus gautas pranešimas.

ValueTask: kodėl ir kaip

Asinchroniniai/laukti operatoriai, kaip ir pajamingumo grąžinimo operatorius, iš metodo generuoja būsenos mašiną, o tai yra naujo objekto sukūrimas, kuris beveik visada nėra svarbus, tačiau retais atvejais gali sukelti problemų. Šis atvejis gali būti tikrai dažnai skambinamas metodas, kalbame apie dešimtis ir šimtus tūkstančių skambučių per sekundę. Jei toks metodas parašytas taip, kad daugeliu atvejų jis grąžina rezultatą aplenkdamas visus laukimo metodus, tai .NET suteikia įrankį tam optimizuoti – ValueTask struktūrą. Kad būtų aišku, pažvelkime į jo naudojimo pavyzdį: yra talpykla, į kurią einame labai dažnai. Jame yra tam tikrų reikšmių, tada mes jas tiesiog grąžiname; jei ne, tada einame į lėtą IO, kad gautume jas. Pastarąjį noriu atlikti asinchroniškai, o tai reiškia, kad visas metodas pasirodo asinchroninis. Taigi, akivaizdus būdas parašyti metodą yra toks:

public async Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return val;
    return await RequestById(id);
}

Dėl noro šiek tiek optimizuoti ir šiek tiek baimindamiesi, ką Roslyn sugeneruos kurdamas šį kodą, galite perrašyti šį pavyzdį taip:

public Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return Task.FromResult(val);
    return RequestById(id);
}

Iš tiesų, optimalus sprendimas šiuo atveju būtų optimizuoti greitąjį kelią, būtent gauti reikšmę iš žodyno be jokių nereikalingų paskirstymų ir apkrovos GC, tuo tarpu tais retais atvejais, kai vis tiek reikia eiti į IO dėl duomenų. , viskas liks plius / minus senuoju būdu:

public ValueTask<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return new ValueTask<string>(val);
    return new ValueTask<string>(RequestById(id));
}

Pažvelkime į šią kodo dalį atidžiau: jei talpykloje yra reikšmė, sukuriame struktūrą, kitaip tikroji užduotis bus įvyniota į prasmingą. Kviečiančiam kodui nesvarbu, kokiu keliu šis kodas buvo paleistas: C# sintaksės požiūriu „ValueTask“ šiuo atveju elgsis taip pat, kaip įprasta užduotis.

TaskSchedulers: užduočių paleidimo strategijų valdymas

Kitas API, kurį norėčiau apsvarstyti, yra klasė Task Scheduler ir jo dariniai. Jau minėjau aukščiau, kad TPL turi galimybę valdyti užduočių paskirstymo gijomis strategijas. Tokios strategijos yra apibrėžtos TaskScheduler klasės palikuonyse. Beveik bet kokią strategiją, kurios jums gali prireikti, galite rasti bibliotekoje. ParallelExtensionsExtras, sukurta Microsoft, bet ne .NET dalis, bet pateikiama kaip Nuget paketas. Trumpai pažvelkime į kai kuriuos iš jų:

  • CurrentThreadTaskScheduler – vykdo užduotis esamoje gijoje
  • LimitedConcurrencyLevelTaskScheduler — riboja užduočių, atliekamų vienu metu, skaičių parametru N, kuris priimtas konstruktoriuje
  • Užsakyta užduočių planavimo priemonė — apibrėžiamas kaip LimitedConcurrencyLevelTaskScheduler(1), todėl užduotys bus vykdomos nuosekliai.
  • WorkStealingTaskScheduler — padargai darbo vagystės požiūris į užduočių paskirstymą. Iš esmės tai yra atskiras ThreadPool. Išsprendžia problemą, kad .NET ThreadPool yra statinė klasė, viena visoms programoms, o tai reiškia, kad jos perkrovimas arba neteisingas naudojimas vienoje programos dalyje gali sukelti šalutinį poveikį kitoje. Be to, labai sunku suprasti tokių defektų priežastį. Tai. Gali prireikti naudoti atskirus WorkStealingTaskSchedulers tose programos dalyse, kur ThreadPool naudojimas gali būti agresyvus ir nenuspėjamas.
  • Eilėje užduočių planavimo priemonė — leidžia atlikti užduotis pagal prioritetinės eilės taisykles
  • ThreadPerTaskScheduler — sukuria atskirą giją kiekvienai joje vykdomai užduočiai. Gali būti naudinga atliekant užduotis, kurių atlikimas užtrunka nenuspėjamai ilgai.

Yra geras detalus straipsnis apie „TaskSchedulers“ „Microsoft“ tinklaraštyje.

Kad būtų patogu derinti viską, kas susiję su užduotimis, „Visual Studio“ turi užduočių langą. Šiame lange galite matyti esamą užduoties būseną ir pereiti prie šiuo metu vykdomos kodo eilutės.

.NET: įrankiai, skirti dirbti su kelių gijų ir asinchronija. 1 dalis

PLinq ir lygiagreti klasė

Be užduočių ir visko, kas apie jas pasakyta, .NET yra dar du įdomūs įrankiai: PLinq (Linq2Parallel) ir Parallel klasė. Pirmasis žada lygiagrečiai vykdyti visas Linq operacijas keliose gijose. Gijų skaičių galima sukonfigūruoti naudojant WithDegreeOfParallelism išplėtimo metodą. Deja, dažniausiai PLinq numatytuoju režimu neturi pakankamai informacijos apie jūsų duomenų šaltinio vidines dalis, kad užtikrintų didelį greičio padidėjimą, kita vertus, bandymo kaina yra labai maža: prieš tai tereikia iškviesti AsParallel metodą. Linq metodų grandinę ir vykdykite veikimo testus. Be to, naudojant skirsnių mechanizmą, PLinq galima perduoti papildomos informacijos apie jūsų duomenų šaltinio pobūdį. Galite paskaityti daugiau čia и čia.

Parallel static klasė suteikia metodus, kaip lygiagrečiai kartoti Foreach rinkinį, vykdyti For kilpą ir vykdyti kelis delegatus lygiagrečiai Invoke. Dabartinės gijos vykdymas bus sustabdytas, kol bus baigti skaičiavimai. Gijų skaičių galima sukonfigūruoti kaip paskutinį argumentą perduodant ParallelOptions. Taip pat galite nurodyti TaskScheduler ir CancellationToken naudodami parinktis.

išvados

Kai pradėjau rašyti šį straipsnį remdamasis savo pranešimo medžiaga ir informacija, kurią surinkau dirbdamas po jo, nesitikėjau, kad jo bus tiek daug. Dabar, kai teksto rengyklė, kurioje rašau šį straipsnį, priekaištingai man pasakys, kad 15 puslapis išėjo, apibendrinsiu tarpinius rezultatus. Kiti triukai, API, vaizdiniai įrankiai ir spąstai bus aptariami kitame straipsnyje.

Išvados:

  • Kad galėtumėte naudotis šiuolaikinių kompiuterių ištekliais, turite žinoti darbo su gijomis, asinchronijos ir lygiagretumo įrankius.
  • .NET šiems tikslams turi daug įvairių įrankių
  • Ne visi jie pasirodė iš karto, todėl dažnai galite rasti senų, tačiau yra būdų, kaip konvertuoti senas API be didelių pastangų.
  • Darbas su gijomis .NET reiškia Thread ir ThreadPool klases
  • Thread.Abort, Thread.Interrupt ir Win32 API TerminateThread metodai yra pavojingi ir nerekomenduojami naudoti. Vietoj to geriau naudoti CancellationToken mechanizmą
  • Srautas yra vertingas išteklius ir jo pasiūla ribota. Reikėtų vengti situacijų, kai gijos yra užsiėmusios įvykių laukimu. Tam patogu naudoti klasę TaskCompletionSource
  • Galingiausi ir pažangiausi .NET įrankiai, skirti dirbti su lygiagretumu ir asinchronija, yra užduotys.
  • C# async/await operatoriai įgyvendina neblokuojančio laukimo koncepciją
  • Galite valdyti užduočių paskirstymą gijose naudodami TaskScheduler gautas klases
  • „ValueTask“ struktūra gali būti naudinga optimizuojant karštuosius kelius ir atminties srautą
  • „Visual Studio“ užduočių ir gijų langai suteikia daug informacijos, naudingos derinant kelių gijų arba asinchroninį kodą
  • PLinq yra puikus įrankis, tačiau jame gali būti nepakankamai informacijos apie jūsų duomenų šaltinį, tačiau tai galima išspręsti naudojant skaidymo mechanizmą
  • Turi būti tęsiama ...

Šaltinis: www.habr.com

Добавить комментарий