.NET: tööriistad mitmelõimega ja asünkroonsusega töötamiseks. 1. osa

Avaldan Habri kohta originaalartikli, mille tõlge postitatakse ettevõttesse ajaveeb.

Vajadus teha midagi asünkroonselt, ootamata tulemust siin ja praegu, või jagada suur töö mitme seda teostava üksuse vahel, oli juba enne arvutite tulekut. Nende tulekuga muutus see vajadus väga käegakatsutavaks. Nüüd, 2019. aastal, kirjutan selle artikli 8-tuumalise Intel Core protsessoriga sülearvutile, millel töötab paralleelselt üle saja protsessi ja veelgi rohkem lõime. Läheduses on paar aastat tagasi ostetud veidi räbal telefon, mille pardal on 8-tuumaline protsessor. Temaatilised ressursid on täis artikleid ja videoid, mille autorid imetlevad selle aasta tipptelefone, millel on 16-tuumaline protsessor. MS Azure pakub 20-tuumalise protsessori ja 128 TB RAM-iga virtuaalmasinat vähem kui 2 dollari eest tunnis. Kahjuks on võimatu saavutada maksimumi ja seda jõudu rakendada, ilma et oleks võimalik juhtida niitide koostoimet.

Terminoloogia

Protsess - OS-i objekt, isoleeritud aadressiruum, sisaldab lõime.
Niit - OS-i objekt, väikseim täitmisüksus, protsessi osa; lõimed jagavad protsessis omavahel mälu ja muid ressursse.
Mitut tööd - OS-i omadus, võimalus käivitada mitut protsessi samaaegselt
Mitmetuumaline - protsessori omadus, võimalus kasutada andmetöötluseks mitut südamikku
Multitöötlus - arvuti omadus, võime töötada korraga mitme protsessoriga füüsiliselt
Mitmelõimeline — protsessi omadus, võime jaotada andmetöötlust mitme lõime vahel.
Paralleelsus - mitme toimingu füüsiliselt üheaegne sooritamine ajaühikus
Asünkroonsus — toimingu sooritamine ilma selle töötlemise lõpetamist ootamata; täitmise tulemust saab hiljem töödelda.

Metafoor

Kõik definitsioonid pole head ja mõned vajavad täiendavat selgitust, seega lisan ametlikult kasutusele võetud terminoloogiale metafoori hommikusöögi valmistamise kohta. Selle metafoori kohaselt on hommikusöögi valmistamine protsess.

Hommikusööki valmistades ma (Protsessor) ma tulen kööki (Arvuti). mul on 2 kätt (Südamikud). Köögis on palju seadmeid (IO): ahi, veekeetja, röster, külmkapp. Lülitan gaasi sisse, panen sellele panni ja valan sinna õli, ootamata, kuni see kuumeneb (asünkroonselt, Non-Blocking-IO-Oota), võtan munad külmkapist välja ja murran taldrikuks, seejärel klopin ühe käega (Lõim nr 1) ja teine ​​(Lõim nr 2) hoides plaati (Jagatud ressurss). Nüüd tahaksin veekeetja sisse lülitada, kuid mul pole piisavalt käsi (Teema Nälgimine) Selle aja jooksul kuumeneb pann (Tulemuse töötlemine), kuhu valan vahustatud. Ma sirutan käe veekeetja poole, lülitan selle sisse ja vaatan rumalalt, kuidas vesi selles keeb (Blokeerimine-IO-Oota), kuigi selle aja jooksul oleks ta võinud pesta taldriku, kus ta omletti vahustas.

Mina keetsin omletti ainult 2 käega ja rohkem mul ei ole, aga samas omleti vahustamise hetkel toimus korraga 3 toimingut: omleti vahustamine, taldriku hoidmine, panni kuumutamine. Protsessor on arvuti kiireim osa, IO on see, mis kõige sagedamini aeglustub, nii et sageli on tõhus lahendus IO-lt andmete vastuvõtmise ajal protsessor millegagi hõivata.

Metafoori jätkates:

  • Kui omleti valmistamise käigus prooviksin ka riideid vahetada, oleks see näide multitegumtööst. Oluline nüanss: arvutid on selles palju paremad kui inimesed.
  • Köök, kus on mitu kokka, näiteks restoranis - mitmetuumaline arvuti.
  • Paljud restoranid kaubanduskeskuse toiduväljakul - andmekeskus

.NET-i tööriistad

.NET oskab hästi lõimedega töötada, nagu ka paljude muude asjadega. Iga uue versiooniga tutvustab see üha uusi tööriistu nendega töötamiseks, uusi abstraktsioonikihte OS-i lõimede kaudu. Abstraktsioonide konstrueerimisega töötades kasutavad raamistiku arendajad lähenemisviisi, mis jätab kõrgetasemelise abstraktsiooni kasutamisel võimaluse minna ühe või mitme taseme võrra madalamale. Enamasti pole see vajalik, tegelikult avab see ukse endale püssist jalga tulistamiseks, kuid mõnikord võib see harvadel juhtudel olla ainus viis probleemi lahendamiseks, mida praegusel abstraktsioonitasemel ei lahendata. .

Tööriistade all pean silmas nii raamistiku kui ka kolmanda osapoole pakette pakutavaid rakendusliideseid (API-sid), aga ka terveid tarkvaralahendusi, mis lihtsustavad mitme lõimega koodiga seotud probleemide otsimist.

Lõime alustamine

Lõimeklass on .NET-i kõige lihtsam klass lõimedega töötamiseks. Konstruktor võtab vastu ühe kahest delegaadist:

  • ThreadStart — parameetreid pole
  • ParametriizedThreadStart – ühe parameetriga objekti tüüpi.

Delegaat käivitatakse vastloodud lõimes pärast Start-meetodi väljakutsumist.Kui konstruktorile anti edasi ParametrizedThreadStart tüüpi delegaat, siis tuleb meetodile Start edastada objekt. Seda mehhanismi on vaja kohaliku teabe edastamiseks voogu. Väärib märkimist, et lõime loomine on kallis toiming ja lõim ise on raske objekt, vähemalt seetõttu, et see eraldab virnale 1 MB mälu ja nõuab suhtlemist OS API-ga.

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

ThreadPool klass esindab basseini kontseptsiooni. .NET-is on lõimekogum inseneritöö ja Microsofti arendajad on teinud palju pingutusi, et tagada selle optimaalne toimimine paljude erinevate stsenaariumide korral.

Üldine kontseptsioon:

Rakenduse käivitumise hetkest loob see taustal reservi mitu lõime ja annab võimaluse need kasutusse võtta. Kui niite kasutatakse sageli ja suurel hulgal, laieneb kogum helistaja vajaduste rahuldamiseks. Kui basseinis pole õigel ajal vabu lõime, ootab see ühe lõime naasmist või loob uue. Sellest järeldub, et lõimede kogum sobib suurepäraselt mõne lühiajalise tegevuse jaoks ja halvasti toimingute jaoks, mis töötavad teenustena kogu rakenduse töötamise ajal.

Puuli lõime kasutamiseks on olemas meetod QueueUserWorkItem, mis võtab vastu WaitCallback tüüpi delegaadi, millel on sama signatuur kui ParametrizedThreadStartil ja sellele edastatud parameeter täidab sama funktsiooni.

ThreadPool.QueueUserWorkItem(...);

Vähemtuntud lõimekogumi meetodit RegisterWaitForSingleObject kasutatakse mitteblokeerivate IO toimingute korraldamiseks. Sellele meetodile üle antud delegaadi kutsutakse välja, kui meetodile edastatud WaitHandle on „vabastatud”.

ThreadPool.RegisterWaitForSingleObject(...)

.NET-il on lõime taimer ja see erineb WinFormsi/WPF-i taimeritest selle poolest, et selle käitleja kutsutakse välja basseinist võetud lõime kaudu.

System.Threading.Timer

On ka üsna eksootiline viis saata delegaat basseinist lõime täitmiseks - BeginInvoke meetod.

DelegateInstance.BeginInvoke

Tahaksin põgusalt peatuda funktsioonil, millele saab kutsuda paljusid ülaltoodud meetodeid – CreateThread from Kernel32.dll Win32 API. Tänu väliste meetodite mehhanismile on võimalus seda funktsiooni kutsuda. Olen sellist üleskutset näinud vaid korra pärandkoodi kohutavas näites ja täpselt seda teinud autori motivatsioon jääb mulle siiani saladuseks.

Kernel32.dll CreateThread

Lõimede vaatamine ja silumine

Teie loodud lõime, kõiki kolmanda osapoole komponente ja .NET-i kogumit saab vaadata Visual Studio Threads aknas. See aken kuvab lõime teavet ainult siis, kui rakendus on silumisrežiimis ja katkestusrežiimis. Siin saate mugavalt vaadata iga lõime pinunimesid ja prioriteete ning lülitada silumine konkreetse lõime vastu. Kasutades klassi Thread atribuuti Priority, saate määrata lõime prioriteedi, mida OC ja CLR tajuvad soovitusena protsessori aja lõimede vahel jagamisel.

.NET: tööriistad mitmelõimega ja asünkroonsusega töötamiseks. 1. osa

Ülesande paralleelteek

Task Parallel Library (TPL) võeti kasutusele .NET 4.0-s. Nüüd on see standard ja peamine tööriist asünkroonsusega töötamiseks. Igasugust koodi, mis kasutab vanemat lähenemist, peetakse pärandiks. TPL-i põhiüksus on System.Threading.Tasks nimeruumist pärit Task klass. Ülesanne on abstraktsioon üle lõime. C# keele uue versiooniga saime elegantse võimaluse tööülesannetega töötamiseks – asünkrooni/ootama operaatorid. Need kontseptsioonid võimaldasid kirjutada asünkroonset koodi nii, nagu see oleks lihtne ja sünkroonne. See võimaldas isegi inimestel, kes ei mõista niitide sisemist tööd, kirjutada neid kasutavaid rakendusi, mis ei külmu pikkade toimingute tegemisel. Asünkrooni/ootmise kasutamine on ühe või isegi mitme artikli teema, kuid püüan selle sisu mõne lausega mõista:

  • async on meetodi modifikaator, mis tagastab ülesande või tühisuse
  • ja await on mitteblokeeriv ülesande ootel operaator.

Veel kord: operaator await vabastab üldjuhul (on erandid) praeguse täitmislõime edasi ja kui ülesanne lõpetab täitmise ja lõime (tegelikult oleks õigem öelda kontekst , kuid sellest lähemalt hiljem) jätkab meetodi täitmist edasi. .NET-i sees on see mehhanism realiseeritud samamoodi nagu tootlus, kui kirjutatud meetod muutub terveks klassiks, mis on olekumasin ja mida saab sõltuvalt nendest olekutest täita eraldi tükkidena. Kõik huvilised saavad kirjutada mis tahes lihtsat koodi kasutades asynс/await, kompileerida ja vaadata komplekti kasutades JetBrains dotPeek koos kompilaatori genereeritud koodiga.

Vaatame Taski käivitamise ja kasutamise võimalusi. Allolevas koodinäites loome uue ülesande, mis ei tee midagi kasulikku (Thread.Sleep (10000)), kuid päriselus peaks see olema keeruline protsessorimahukas töö.

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
}

Ülesanne luuakse mitme valikuga:

  • LongRunning on vihje sellele, et ülesanne ei saa kiiresti lõpule, mis tähendab, et tasub kaaluda mitte basseinist lõime võtmist, vaid selle ülesande jaoks eraldi lõime loomist, et teisi mitte kahjustada.
  • AttachedToParent – ​​ülesandeid saab järjestada hierarhiasse. Kui seda võimalust kasutati, võib Ülesanne olla olekus, kus see on ise täidetud ja ootab oma laste täitmist.
  • PreferFairness – tähendab, et täitmiseks saadetud ülesanded oleks parem täita varem, enne kui hiljem saadetud. Kuid see on vaid soovitus ja tulemus pole garanteeritud.

Teine meetodile edastatud parameeter on CancellationToken. Toimingu tühistamise õigeks käsitlemiseks pärast selle käivitamist tuleb käivitatav kood täita oleku CancellationToken kontrollidega. Kui kontrolle pole, saab CancellationTokenSource objektil kutsutud meetod Cancel ülesande täitmise peatada ainult enne selle käivitamist.

Viimane parameeter on TaskScheduler tüüpi planeerijaobjekt. See klass ja selle järglased on loodud ülesannete lõimede vahel jaotamise strateegiate juhtimiseks; vaikimisi käivitatakse ülesanne kogumi juhuslikul lõimel.

Loodud ülesandele rakendatakse operaator await, mis tähendab, et selle järele kirjutatud kood, kui see on olemas, käivitatakse samas kontekstis (sageli tähendab see samas lõimes) kui kood enne ootamist.

Meetod on märgitud asünkroonituks, mis tähendab, et see võib kasutada ooteoperaatorit, kuid kutsuv kood ei saa täitmist oodata. Kui selline funktsioon on vajalik, peab meetod tagastama Task. Async void märgistusega meetodid on üsna levinud: reeglina on need sündmuste käitlejad või muud meetodid, mis töötavad tulekahju ja unusta põhimõttel. Kui peate mitte ainult andma võimaluse oodata täitmise lõpuni, vaid ka tagastama tulemuse, peate kasutama ülesannet.

Ülesandes, mille meetod StartNew tagastas, nagu ka mis tahes muus, saate kutsuda ConfigureAwait meetodit vale parameetriga, seejärel jätkub täitmine pärast ootamist mitte jäädvustatud kontekstis, vaid suvalises kontekstis. Seda tuleks teha alati, kui täitmise kontekst pole pärast ootamist koodi jaoks oluline. See on ka MS-i soovitus koodi kirjutamisel, mis tarnitakse pakendatud teeki.

Vaatleme veidi lähemalt, kuidas saate ülesande täitmist oodata. Allpool on näide koodist koos kommentaaridega selle kohta, millal on ootus tinglikult hästi tehtud ja millal tinglikult halvasti.

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
}

Esimeses näites ootame ülesande lõpuleviimist ilma kutsuvat lõime blokeerimata; tuleme tulemuse töötlemise juurde tagasi alles siis, kui see on juba olemas; seni jäetakse kutsuv lõim omaette.

Teises variandis blokeerime kutsuva lõime, kuni meetodi tulemus on arvutatud. See on halb mitte ainult seetõttu, et oleme hõivanud lõime, programmi nii väärtusliku ressursi, lihtsalt jõudeolekuga, vaid ka seetõttu, et kui kutsutava meetodi kood sisaldab ootamist ja sünkroonimiskontekst nõuab pärast kutsuva lõime naasmist. oota, siis saame ummikseisu : Kutsuv lõim ootab asünkroonse meetodi tulemuse arvutamist, asünkroonne meetod üritab tulutult jätkata oma täitmist kutsuvas lõimes.

Selle lähenemisviisi teine ​​puudus on keeruline vigade käsitlemine. Fakt on see, et asünkroonse koodi tõrkeid asünkroonse koodi kasutamisel on väga lihtne käsitleda – need käituvad samamoodi nagu kood oleks sünkroonne. Kui rakendame ülesandele sünkroonse ootamise eksortsismi, muutub algne erand AggregateExceptioniks, st. Erandi käsitlemiseks peate uurima InnerException tüüpi ja kirjutama ise ühe püüdmisploki sisse if-ahela või kasutama konstrueerimisel püüdmist, mitte C#-maailmas tuttavama püüdmisplokkide ahela asemel.

Kolmas ja viimane näide on samuti märgitud halvaks samal põhjusel ja sisaldavad kõiki samu probleeme.

Meetodid WhenAny ja WhenAll on ülesannete rühma ootamiseks äärmiselt mugavad; need koondavad ülesannete rühma üheks, mis käivitub kas siis, kui rühma ülesanne esmakordselt käivitatakse või kui kõik need on täitmise lõpetanud.

Lõngade peatamine

Erinevatel põhjustel võib osutuda vajalikuks vool pärast selle algust peatada. Selleks on mitmeid viise. Lõimeklassil on kaks sobiva nimega meetodit: Katkesta и Vahele segama. Esimest pole väga soovitatav kasutada, kuna pärast selle helistamist suvalisel hetkel, mis tahes käsu töötlemise ajal, tehakse erand ThreadAbortedException. Te ei eelda, et selline erand tuleb suvalise täisarvu muutuja suurendamisel, eks? Ja seda meetodit kasutades on see väga reaalne olukord. Kui teil on vaja takistada CLR-il sellist erandit teatud koodijaotises genereerimast, saate selle kõnedeks mähkida Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Iga lõplikku plokki kirjutatud kood on mähitud sellistesse kõnedesse. Sel põhjusel võib raamistiku koodi sügavustest leida tühja katsega plokke, kuid mitte tühja katsega. Microsoft ei soovita seda meetodit nii palju, et nad ei lisanud seda .net-tuuma.

Katkestamise meetod töötab prognoositavamalt. See võib lõime erandiga katkestada ThreadInterruptedException ainult neil hetkedel, kui niit on ooteseisundis. See siseneb sellesse olekusse rippudes, oodates WaitHandle'i, lukustust või pärast funktsiooni Thread.Sleep helistamist.

Mõlemad ülalkirjeldatud variandid on oma ettearvamatuse tõttu halvad. Lahenduseks on struktuuri kasutamine CancellationToken ja klass CancellationTokenSource. Asi on selles: luuakse klassi CancellationTokenSource eksemplar ja ainult see, kellele see kuulub, saab toimingu peatada, kutsudes välja meetodi tühistama. Operatsioonile endale edastatakse ainult CancellationToken. CancellationTokeni omanikud ei saa toimingut ise tühistada, vaid saavad ainult kontrollida, kas toiming on tühistatud. Selle jaoks on Boole'i ​​omadus IsCancellationRequested ja meetod ThrowIfCancelRequested. Viimane teeb erandi TaskCancelledException kui CancellationTokeni eksemplaril, mille parrot on tehtud, kutsuti välja meetod Cancel. Ja seda meetodit soovitan kasutada. See on eelmiste valikute paremus, saavutades täieliku kontrolli selle üle, millal saab eranditoimingu katkestada.

Kõige jõhkram võimalus lõime peatamiseks on kutsuda Win32 API funktsiooni TerminateThread. CLR-i käitumine pärast selle funktsiooni kutsumist võib olla ettearvamatu. MSDN-is on selle funktsiooni kohta kirjutatud järgmist: "TerminateThread on ohtlik funktsioon, mida tuleks kasutada ainult kõige äärmuslikumatel juhtudel. “

Pärand API teisendamine ülesandepõhiseks, kasutades FromAsynci meetodit

Kui teil on õnn töötada projekti kallal, mida alustati pärast Tasksi kasutuselevõttu ja mis ei tekitanud enamuse arendajate jaoks vaikset õudust, siis ei pea te tegelema paljude vanade API-dega, nii kolmandate osapoolte kui ka teie meeskonnaga. on varem piinanud. Õnneks võttis meie eest hoolt .NET Frameworki meeskond, kuigi võib-olla oli eesmärk meie enda eest hoolitseda. Olgu kuidas on, aga .NET-il on mitmeid tööriistu vanade asünkroonse programmeerimise lähenemisviisidega kirjutatud koodi valutuks teisendamiseks uueks. Üks neist on TaskFactory meetod FromAsync. Allolevas koodinäites mähkin selle meetodi abil klassi WebRequest vanad asünkroonimismeetodid ülesandesse.

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

See on vaid näide ja tõenäoliselt ei pea te seda sisseehitatud tüüpidega tegema, kuid kõik vanad projektid kubisevad BeginDoSomethingi meetoditest, mis tagastavad selle vastuvõtvad meetodid IAsyncResult ja EndDoSomething.

Teisendage pärand API ülesandepõhiseks, kasutades klassi TaskCompletionSource

Teine oluline tööriist, millega arvestada, on klass TaskCompletionSource. Funktsioonide, eesmärgi ja tööpõhimõtte poolest võib see mõneti meenutada ThreadPool klassi meetodit RegisterWaitForSingleObject, millest eespool kirjutasin. Seda klassi kasutades saate lihtsalt ja mugavalt vanad asünkroonsed API-d Tasksisse mässida.

Ütlete, et olen juba rääkinud selleks otstarbeks mõeldud TaskFactory klassi FromAsync meetodist. Siinkohal peame meenutama kogu .net-i asünkroonsete mudelite väljatöötamise ajalugu, mida Microsoft on viimase 15 aasta jooksul pakkunud: enne tegumipõhist asünkroonmustrit (TAP) oli asünkroonne programmeerimismuster (APP), mis puudutas meetodeid AlgamaDoSomething naaseb IAsyncResult ja meetodid LõppDoSomething, mis seda aktsepteerib ja nende aastate pärandi jaoks on FromAsynci meetod lihtsalt täiuslik, kuid aja jooksul asendati see sündmustepõhise asünkroonse mustriga (EAP), mis eeldas, et asünkroonse toimingu lõppedes tõstatatakse sündmus.

TaskCompletionSource sobib suurepäraselt sündmuste mudeli ümber ehitatud ülesannete ja pärand API-de pakkimiseks. Selle töö olemus on järgmine: selle klassi objektil on Task tüüpi avalik atribuut, mille olekut saab juhtida klassi TaskCompletionSource meetoditega SetResult, SetException jne. Kohtades, kus sellele ülesandele rakendati ooteoperaatorit, käivitatakse see või see ebaõnnestub erandiga, olenevalt TaskCompletionSource'ile rakendatud meetodist. Kui see ikka pole selge, vaatame seda koodinäidet, kus mõni vana EAP API on mähitud tegumisse, kasutades TaskCompletionSource'i: kui sündmus käivitub, viiakse ülesanne olekusse Lõpetatud ja meetod, mis rakendas ooteoperaatorit. sellele ülesandele jätkab selle täitmist pärast objekti kättesaamist kaasa.

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 näpunäited ja nipid

Vanade API-de mähkimine pole kõik, mida saab teha TaskCompletionSource'i abil. Selle klassi kasutamine avab huvitava võimaluse kujundada erinevaid API-sid Tasksis, mis ei hõivata lõime. Ja voog, nagu mäletame, on kallis ressurss ja nende arv on piiratud (peamiselt RAM-i mahuga). Seda piirangut saab hõlpsasti saavutada, arendades näiteks keeruka äriloogikaga laaditud veebirakendust. Mõelgem võimalustele, millest ma räägin sellise nipi nagu Long-Polling rakendamisel.

Lühidalt, triki olemus on järgmine: peate API-lt saama teavet mõne tema poolel toimuva sündmuse kohta, samas kui API ei saa mingil põhjusel sündmusest teatada, vaid saab ainult oleku tagastada. Nende näide on kõik API-d, mis on ehitatud HTTP peale enne WebSocketi aega või kui seda tehnoloogiat ei olnud mingil põhjusel võimalik kasutada. Klient saab küsida HTTP-serverilt. HTTP-server ei saa ise kliendiga sidet alustada. Lihtne lahendus on küsitleda serverit taimeriga, kuid see tekitab serverile lisakoormust ja lisaviivitust keskmiselt TimerInterval / 2. Sellest mööda saamiseks leiutati nipp nimega Long Polling, mis hõlmab vastuse viivitamist serverilt. kuni Timeout aegub või sündmuse toimumiseni. Kui sündmus toimub, siis seda töödeldakse, kui mitte, saadetakse päring uuesti.

while(!eventOccures && !timeoutExceeded)  {

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

Kuid selline lahendus osutub kohutavaks kohe, kui üritust ootavate klientide arv suureneb, sest... Iga selline klient hõivab terve lõime, mis ootab sündmust. Jah, ja sündmuse käivitamisel saame täiendava 1 ms viivituse, enamasti pole see oluline, kuid miks teha tarkvara halvemaks, kui see olla saab? Kui eemaldada Thread.Sleep(1), siis asjata laadime ühe protsessori tuuma 100% tühikäigul, pöörledes kasutu tsükliga. TaskCompletionSource'i abil saate selle koodi hõlpsalt ümber teha ja lahendada kõik ülaltoodud probleemid:

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

See kood pole tootmisvalmis, vaid lihtsalt demo. Selle reaalseks kasutamiseks peate vähemalt lahendama ka olukorra, kui sõnum saabub ajal, mil keegi seda ei oota: sel juhul peaks meetod AsseptMessageAsync tagastama juba täidetud ülesande. Kui see on kõige levinum juhtum, võite mõelda ValueTaski kasutamisele.

Kui saame sõnumipäringu, loome ja asetame sõnastikku TaskCompletionSource ning seejärel ootame, mis juhtub kõigepealt: määratud ajaintervall aegub või sõnum saabub.

ValueTask: miks ja kuidas

Asünkroniseerimise/ootamise operaatorid, nagu ka tootlus tagastamise operaator, genereerivad meetodist olekumasina ja see on uue objekti loomine, mis peaaegu alati ei ole oluline, kuid harvadel juhtudel võib see probleemi tekitada. See juhtum võib olla meetod, millele helistatakse tõesti sageli, me räägime kümnetest ja sadadest tuhandetest kõnedest sekundis. Kui selline meetod on kirjutatud nii, et enamikul juhtudel tagastab see kõigist ootamismeetoditest mööda minnes tulemuse, siis .NET pakub selle optimeerimiseks tööriista – ValueTask struktuuri. Selguse huvides vaatame selle kasutamise näidet: on vahemälu, mida me väga sageli külastame. See sisaldab mõningaid väärtusi ja siis me lihtsalt tagastame need; kui ei, siis läheme nende hankimiseks mõnele aeglasele IO-le. Ma tahan viimast teha asünkroonselt, mis tähendab, et kogu meetod osutub asünkroonseks. Seega on ilmselge viis meetodi kirjutamiseks järgmine:

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

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

Kuna soovite veidi optimeerida ja kardate seda, mida Roslyn selle koodi koostamisel genereerib, saate selle näite ümber kirjutada järgmiselt:

public Task<string> GetById(int id) {

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

Tõepoolest, sel juhul oleks optimaalne lahendus kiirtee optimeerimine, nimelt väärtuse hankimine sõnastikust ilma tarbetute jaotusteta ja GC-le koormuseta, samal ajal kui neil harvadel juhtudel, kui peame siiski andmete saamiseks minema IO-sse. , jääb kõik vanaviisi pluss/miinus:

public ValueTask<string> GetById(int id) {

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

Vaatame seda koodiosa lähemalt: kui vahemälus on väärtus, loome struktuuri, vastasel juhul mähitakse tegelik ülesanne tähendusrikkasse. Kutsuval koodil pole vahet, millisel teel see kood käivitati: C# süntaksi seisukohast käitub ValueTask sel juhul samamoodi nagu tavaline ülesanne.

TaskSchedulers: ülesannete käivitamise strateegiate haldamine

Järgmine API, mida tahaksin kaaluda, on klass TaskScheduler ja selle tuletised. Mainisin juba eespool, et TPL-il on võimalus hallata ülesannete lõimede vahel levitamise strateegiaid. Sellised strateegiad on määratletud klassi TaskScheduler järglastes. Peaaegu kõik strateegiad, mida vajate, leiate raamatukogust. ParallelExtensionsExtras, mille on välja töötanud Microsoft, kuid mis ei kuulu .NET-i, vaid tarnitakse Nugeti paketina. Vaatame lühidalt mõnda neist:

  • CurrentThreadTaskScheduler — käivitab aktiivse lõime ülesanded
  • LimitedConcurrencyLevelTaskScheduler — piirab konstruktoris aktsepteeritud parameetri N abil samaaegselt täidetavate ülesannete arvu
  • TellitudTaskScheduler — on määratletud kui LimitedConcurrencyLevelTaskScheduler(1), nii et ülesandeid täidetakse järjestikku.
  • WorkStealingTaskScheduler - tööriistad töö-varastamine lähenemine ülesannete jaotusele. Sisuliselt on see eraldi ThreadPool. Lahendab probleemi, et .NET ThreadPool on staatiline klass, üks kõigi rakenduste jaoks, mis tähendab, et selle ülekoormamine või vale kasutamine ühes programmiosas võib põhjustada kõrvalmõjusid teises. Pealegi on selliste defektide põhjust äärmiselt raske mõista. See. Programmi osades, kus ThreadPooli kasutamine võib olla agressiivne ja ettearvamatu, võib tekkida vajadus kasutada eraldi WorkStealingTaskScheduleri.
  • QueuedTaskScheduler — võimaldab täita ülesandeid prioriteedijärjekorra reeglite järgi
  • ThreadPerTaskScheduler — loob iga selles täidetava ülesande jaoks eraldi lõime. Võib olla kasulik ülesannete puhul, mille täitmine võtab ettearvamatult kaua aega.

Seal on hea detailne artikkel TaskScheduleri kohta Microsofti ajaveebis.

Kõigi ülesannetega seonduva mugavaks silumiseks on Visual Studiol aken Tasks. Selles aknas näete ülesande praegust olekut ja saate hüpata hetkel käivitatavale koodireale.

.NET: tööriistad mitmelõimega ja asünkroonsusega töötamiseks. 1. osa

PLinq ja paralleelklass

Lisaks ülesannetele ja kõigele, mida nende kohta öeldakse, on .NET-is veel kaks huvitavat tööriista: PLinq (Linq2Parallel) ja Parallel klass. Esimene lubab kõigi Linqi operatsioonide paralleelset täitmist mitmel lõimel. Lõimede arvu saab konfigureerida laiendusmeetodi WithDegreeOfParallelism abil. Kahjuks ei ole PLinqil vaikerežiimis enamasti piisavalt teavet teie andmeallika sisemiste kohta, et pakkuda märkimisväärset kiiruse kasvu, teisest küljest on proovimise hind väga madal: enne peate lihtsalt kutsuma AsParallel meetodi. Linqi meetodite ahela ja käivitage jõudlustestid. Lisaks on partitsioonide mehhanismi abil võimalik edastada PLinqile täiendavat teavet oma andmeallika olemuse kohta. Saate rohkem lugeda siin и siin.

Parallel-staatiline klass pakub meetodeid Foreachi kogu paralleelseks itereerimiseks, Fore tsükli käivitamiseks ja mitme delegaadi käivitamiseks paralleelselt Invoke'iga. Praeguse lõime täitmine peatatakse, kuni arvutused on lõpetatud. Lõimede arvu saab konfigureerida, edastades viimase argumendina ParallelOptions. Suvandite abil saate määrata ka TaskScheduleri ja CancellationToken.

Järeldused

Kui hakkasin seda artiklit kirjutama oma aruande materjalide ja pärast seda töö käigus kogutud teabe põhjal, ei oodanud ma, et seda nii palju saab olema. Nüüd, kui tekstiredaktor, millesse ma seda artiklit kirjutan, ütleb mulle etteheitvalt, et lehekülg 15 on kadunud, teen vahetulemused kokkuvõtte. Teisi nippe, API-sid, visuaalseid tööriistu ja lõkse käsitletakse järgmises artiklis.

Järeldused:

  • Kaasaegsete personaalarvutite ressursside kasutamiseks peate teadma lõimedega töötamise tööriistu, asünkroonsust ja paralleelsust.
  • .NETil on nendeks eesmärkideks palju erinevaid tööriistu
  • Kõik need ei ilmunud korraga, nii et sageli võite leida pärandaid, kuid vanu API-sid saab ilma suurema vaevata teisendada.
  • Lõimedega töötamist .NET-is esindavad klassid Thread ja ThreadPool
  • Meetodid Thread.Abort, Thread.Interrupt ja Win32 API TerminateThread on ohtlikud ja neid ei soovitata kasutada. Selle asemel on parem kasutada CancellationToken mehhanismi
  • Flow on väärtuslik ressurss ja selle pakkumine on piiratud. Vältida tuleks olukordi, kus niidid on hõivatud sündmuste ootamisega. Selleks on mugav kasutada klassi TaskCompletionSource
  • Kõige võimsamad ja täiustatud .NET-i tööriistad paralleelsuse ja asünkroonsusega töötamiseks on Tasks.
  • C# async/await operaatorid rakendavad mitteblokeeriva ootamise kontseptsiooni
  • TaskSchedulerist tuletatud klasside abil saate juhtida ülesannete jaotust lõimede vahel
  • ValueTaski struktuur võib olla kasulik kiirteede ja mäluliikluse optimeerimisel
  • Visual Studio ülesannete ja lõimede aknad pakuvad palju kasulikku teavet mitme lõimega või asünkroonse koodi silumiseks
  • PLinq on lahe tööriist, kuid sellel ei pruugi olla piisavalt teavet teie andmeallika kohta, kuid seda saab parandada partitsioonimehhanismi abil
  • Jätkub ...

Allikas: www.habr.com

Lisa kommentaar