.NET: Gereedskap om met multithreading en asinchronie te werk. Deel 1

Ek publiseer die oorspronklike artikel oor Habr, waarvan die vertaling in die korporatiewe pos geplaas word blogpos.

Die behoefte om iets asinchroon te doen, sonder om hier en nou vir die resultaat te wag, of om groot werk te verdeel tussen verskeie eenhede wat dit uitvoer, het bestaan ​​voor die koms van rekenaars. Met hul koms het hierdie behoefte baie tasbaar geword. Nou, in 2019, tik ek hierdie artikel op 'n skootrekenaar met 'n 8-kern Intel Core-verwerker, waarop meer as honderd prosesse parallel loop, en selfs meer drade. Naby is daar 'n effens skamele foon wat 'n paar jaar gelede gekoop is, dit het 'n 8-kern verwerker aan boord. Tematiese hulpbronne is vol artikels en video's waar hul skrywers vanjaar se vlagskip-slimfone met 16-kern-verwerkers bewonder. MS Azure bied 'n virtuele masjien met 'n 20-kernverwerker en 128 TB RAM vir minder as $2/uur. Ongelukkig is dit onmoontlik om die maksimum te onttrek en hierdie krag te benut sonder om die interaksie van drade te bestuur.

terminologie

Proses - OS-objek, geïsoleerde adresruimte, bevat drade.
Draad - 'n OS-objek, die kleinste eenheid van uitvoering, deel van 'n proses, drade deel geheue en ander hulpbronne onder mekaar binne 'n proses.
multitasking - OS eiendom, die vermoë om verskeie prosesse gelyktydig uit te voer
Veelkern - 'n eienskap van die verwerker, die vermoë om verskeie kerne vir dataverwerking te gebruik
Multiverwerking - 'n eienskap van 'n rekenaar, die vermoë om gelyktydig met verskeie verwerkers fisies te werk
Multithreading — 'n eienskap van 'n proses, die vermoë om dataverwerking tussen verskeie drade te versprei.
Parallelisme - die uitvoering van verskeie aksies fisies gelyktydig per eenheid van tyd
Asinchronie — uitvoering van 'n operasie sonder om te wag vir die voltooiing van hierdie verwerking; die resultaat van die uitvoering kan later verwerk word.

metafoor

Nie alle definisies is goed nie en sommige het addisionele verduideliking nodig, so ek sal 'n metafoor oor ontbyt kook by die formeel ingevoerde terminologie voeg. Om ontbyt te kook in hierdie metafoor is 'n proses.

Terwyl ek soggens ontbyt voorberei het, het ek (CPU) Ek kom kombuis toe (rekenaar). Ek het 2 hande (Cores). Daar is 'n aantal toestelle in die kombuis (IO): oond, ketel, broodrooster, yskas. Ek skakel die gas aan, sit 'n braaipan daarop en gooi olie daarin sonder om te wag dat dit warm word (asynchronies, Nie-Blokkerende-IO-Wag), haal ek die eiers uit die yskas en breek dit in 'n bord, klits dit dan met een hand (Draad #1), en tweede (Draad #2) wat die bord vashou (Gedeelde Hulpbron). Nou wil ek graag die ketel aanskakel, maar ek het nie genoeg hande nie (Draad Verhongering) Gedurende hierdie tyd word die braaipan warm (Verwerk die resultaat) waarin ek gooi wat ek geklits het. Ek gryp na die ketel en skakel dit aan en kyk dom hoe die water daarin kook (Blokkering-IO-Wag), hoewel hy gedurende hierdie tyd die bord kon was waar hy die omelet geklits het.

Ek het 'n omelet met net 2 hande gaargemaak, en ek het nie meer nie, maar terselfdertyd, op die oomblik van die klits van die omelet, het 3 bewerkings gelyktydig plaasgevind: klits die omelet, hou die bord vas, verhit die braaipan Die SVE is die vinnigste deel van die rekenaar, IO is wat gewoonlik alles vertraag, so dikwels is 'n effektiewe oplossing om die SVE met iets te beset terwyl data vanaf IO ontvang word.

Gaan voort met die metafoor:

  • As ek in die proses van die voorbereiding van 'n omelet ook sou probeer om klere te ruil, sou dit 'n voorbeeld van multitasking wees. 'n Belangrike nuanse: rekenaars is baie beter hiermee as mense.
  • 'n Kombuis met verskeie sjefs, byvoorbeeld in 'n restaurant - 'n multi-core rekenaar.
  • Baie restaurante in 'n koshof in 'n winkelsentrum - datasentrum

.NET Gereedskap

.NET is goed om met drade te werk, soos met baie ander dinge. Met elke nuwe weergawe stel dit meer en meer nuwe gereedskap bekend om daarmee te werk, nuwe lae van abstraksie oor OS-drade. Wanneer daar met die konstruksie van abstraksies gewerk word, gebruik raamwerkontwikkelaars 'n benadering wat die geleentheid laat om, wanneer 'n hoëvlak-abstraksie gebruik word, een of meer vlakke onder te daal. Dikwels is dit nie nodig nie, in werklikheid maak dit die deur oop om jouself met 'n haelgeweer in die voet te skiet, maar soms, in seldsame gevalle, is dit dalk die enigste manier om 'n probleem op te los wat nie op die huidige vlak van abstraksie opgelos word nie. .

Met gereedskap bedoel ek beide toepassingsprogrammeringskoppelvlakke (API's) wat deur die raamwerk verskaf word en derdeparty-pakkette, sowel as volledige sagteware-oplossings wat die soektog na enige probleme wat met multi-threaded-kode verband hou, vergemaklik.

Begin 'n draad

Die Thread-klas is die mees basiese klas in .NET om met drade te werk. Die konstruktor aanvaar een van twee afgevaardigdes:

  • ThreadStart - Geen parameters nie
  • ParametrizedThreadStart - met een parameter van tipe voorwerp.

Die afgevaardigde sal in die nuutgeskepte draad uitgevoer word nadat die Start-metode geroep is. As 'n afgevaardigde van die tipe ParametrizedThreadStart na die konstruktor deurgegee is, moet 'n objek na die Start-metode deurgegee word. Hierdie meganisme is nodig om enige plaaslike inligting na die stroom oor te dra. Dit is opmerklik dat die skep van 'n draad 'n duur operasie is, en die draad self is 'n swaar voorwerp, ten minste omdat dit 1 MB geheue op die stapel toeken en interaksie met die OS API vereis.

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

Die ThreadPool-klas verteenwoordig die konsep van 'n swembad. In .NET is die draadpoel 'n stuk ingenieurswese, en die ontwikkelaars by Microsoft het baie moeite gedoen om seker te maak dit werk optimaal in 'n wye verskeidenheid scenario's.

Algemene konsep:

Vanaf die oomblik dat die toepassing begin, skep dit verskeie drade in reserwe op die agtergrond en bied die vermoë om dit vir gebruik te neem. As drade gereeld en in groot getalle gebruik word, brei die swembad uit om aan die beller se behoeftes te voldoen. Wanneer daar geen vrye drade op die regte tyd in die swembad is nie, sal dit óf wag vir een van die drade om terug te keer, óf 'n nuwe een skep. Dit volg dat die draadpoel ideaal is vir sommige korttermyn-aksies en swak geskik is vir bedrywighede wat deur die hele werking van die toepassing as dienste loop.

Om 'n draad uit die swembad te gebruik, is daar 'n QueueUserWorkItem-metode wat 'n afgevaardigde van die tipe WaitCallback aanvaar, wat dieselfde handtekening as ParametrizedThreadStart het, en die parameter wat daaraan oorgedra word, voer dieselfde funksie uit.

ThreadPool.QueueUserWorkItem(...);

Die minder bekende draadpoelmetode RegisterWaitForSingleObject word gebruik om nie-blokkerende IO-operasies te organiseer. Die afgevaardigde wat na hierdie metode oorgedra is, sal geroep word wanneer die WaitHandle wat na die metode oorgedra is "Vrygestel" is.

ThreadPool.RegisterWaitForSingleObject(...)

.NET het 'n thread timer en dit verskil van WinForms/WPF timers deurdat sy hanteerder geroep sal word op 'n draad wat uit die swembad geneem is.

System.Threading.Timer

Daar is ook 'n taamlik eksotiese manier om 'n afgevaardigde vir uitvoering na 'n draad uit die swembad te stuur - die BeginInvoke-metode.

DelegateInstance.BeginInvoke

Ek wil kortliks stilstaan ​​by die funksie waarna baie van die bogenoemde metodes genoem kan word - CreateThread van Kernel32.dll Win32 API. Daar is 'n manier, danksy die meganisme van eksterne metodes, om hierdie funksie te noem. Ek het so 'n oproep nog net een keer in 'n verskriklike voorbeeld van nalatenskapkode gesien, en die motivering van die skrywer wat presies dit gedoen het, bly vir my steeds 'n raaisel.

Kernel32.dll CreateThread

Bekyk en ontfout drade

Drade wat deur jou geskep is, alle derdeparty-komponente en die .NET-poel kan in die Threads-venster van Visual Studio bekyk word. Hierdie venster sal slegs draadinligting vertoon wanneer die toepassing onder ontfouting en in Breekmodus is. Hier kan jy gerieflik die stapelname en prioriteite van elke draad sien, en ontfouting na 'n spesifieke draad oorskakel. Deur die Priority-eienskap van die Thread-klas te gebruik, kan jy die prioriteit van 'n draad stel, wat die OC en CLR sal sien as 'n aanbeveling wanneer die verwerkertyd tussen drade verdeel word.

.NET: Gereedskap om met multithreading en asinchronie te werk. Deel 1

Taak Parallelle Biblioteek

Task Parallel Library (TPL) is in .NET 4.0 bekendgestel. Nou is dit die standaard en die belangrikste instrument om met asinchronie te werk. Enige kode wat 'n ouer benadering gebruik, word as nalatenskap beskou. Die basiese eenheid van TPL is die Taakklas van die System.Threading.Tasks-naamruimte. 'n Taak is 'n abstraksie oor 'n draad. Met die nuwe weergawe van die C#-taal het ons 'n elegante manier gekry om met Take te werk - async/wag-operateurs. Hierdie konsepte het dit moontlik gemaak om asinchrone kode te skryf asof dit eenvoudig en sinchronies is, dit het dit selfs vir mense met min begrip van die interne werking van drade moontlik gemaak om toepassings te skryf wat dit gebruik, toepassings wat nie vries wanneer hulle lang bewerkings uitvoer nie. Die gebruik van async/wag ​​is 'n onderwerp vir een of selfs verskeie artikels, maar ek sal probeer om die kern daarvan in 'n paar sinne te kry:

  • async is 'n wysiger van 'n metode wat Task of void terugstuur
  • en wag is 'n nie-blokkerende Taak wag operateur.

Weereens: die wag-operateur, in die algemene geval (daar is uitsonderings), sal die huidige draad van uitvoering verder vrystel, en wanneer die Taak klaar is met sy uitvoering, en die draad (in werklikheid, dit sal meer korrek wees om die konteks te sê , maar later meer daaroor) sal voortgaan om die metode verder uit te voer. Binne .NET word hierdie meganisme geïmplementeer op dieselfde manier as opbrengs opbrengs, wanneer die geskrewe metode verander in 'n hele klas, wat 'n staatsmasjien is en in afsonderlike stukke uitgevoer kan word, afhangende van hierdie state. Enigiemand wat belangstel, kan enige eenvoudige kode skryf met asynс/wag, die samestelling saamstel en bekyk met JetBrains dotPeek met samesteller-gegenereerde kode geaktiveer.

Kom ons kyk na opsies om Taak te begin en te gebruik. In die kodevoorbeeld hieronder skep ons 'n nuwe taak wat niks nuttigs doen nie (Draad.Slaap(10000 XNUMX)), maar in die werklike lewe behoort dit ingewikkelde SVE-intensiewe werk te wees.

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
}

'n Taak word geskep met 'n aantal opsies:

  • LongRunning is 'n wenk dat die taak nie vinnig voltooi sal word nie, wat beteken dit kan die moeite werd wees om te oorweeg om nie 'n draad uit die swembad te neem nie, maar 'n aparte een vir hierdie taak te skep om nie ander te benadeel nie.
  • AttachedToParent - Take kan in 'n hiërargie gerangskik word. As hierdie opsie gebruik is, kan die taak in 'n toestand wees waar dit self voltooi is en wag vir die uitvoering van sy kinders.
  • PreferFairness - beteken dat dit beter sal wees om take wat vir uitvoering gestuur is, vroeër uit te voer voor dié wat later gestuur word. Maar dit is net 'n aanbeveling en resultate word nie gewaarborg nie.

Die tweede parameter wat na die metode oorgedra word, is CancellationToken. Om die kansellasie van 'n operasie korrek te hanteer nadat dit begin het, moet die kode wat uitgevoer word gevul word met tjeks vir die CancellationToken-toestand. As daar geen kontrole is nie, sal die Kanselleer-metode wat op die CancellationTokenSource-objek geroep word, die uitvoering van die taak eers kan stop voordat dit begin.

Die laaste parameter is 'n skeduleerderobjek van tipe TaskScheduler. Hierdie klas en sy afstammelinge is ontwerp om strategieë te beheer vir die verspreiding van take oor drade; by verstek sal die taak op 'n ewekansige draad vanaf die poel uitgevoer word.

Die wag-operateur word toegepas op die geskepte taak, wat beteken dat die kode wat daarna geskryf is, as daar een is, in dieselfde konteks uitgevoer sal word (dit beteken dikwels op dieselfde draad) as die kode voor wag.

Die metode is gemerk as asinkroniseer leeg, wat beteken dat dit die wag-operateur kan gebruik, maar die oproepkode sal nie vir uitvoering kan wag nie. As so 'n kenmerk nodig is, moet die metode Taak terugstuur. Metodes wat asinc ongeldig gemerk is, is redelik algemeen: in die reël is dit gebeurtenishanteerders of ander metodes wat op die vuur-en-vergeet-beginsel werk. As jy nie net die geleentheid moet gee om te wag tot die einde van uitvoering nie, maar ook die resultaat moet terugstuur, moet jy Taak gebruik.

Op die taak wat die StartNew-metode teruggestuur het, sowel as op enige ander, kan u die ConfigureAwait-metode met die valse parameter noem, dan sal uitvoering na wag voortgaan nie op die vasgelê konteks nie, maar op 'n arbitrêre een. Dit moet altyd gedoen word wanneer die uitvoering konteks nie belangrik is vir die kode na wag. Dit is ook 'n aanbeveling van MS wanneer kode geskryf word wat verpak in 'n biblioteek afgelewer sal word.

Kom ons stil 'n bietjie meer oor hoe jy kan wag vir die voltooiing van 'n taak. Hieronder is 'n voorbeeld van kode, met kommentaar oor wanneer die verwagting voorwaardelik goed gedoen word en wanneer dit voorwaardelik swak gedoen word.

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
}

In die eerste voorbeeld wag ons vir die taak om te voltooi sonder om die oproepdraad te blokkeer; ons sal terugkeer na die verwerking van die resultaat slegs wanneer dit reeds daar is; tot dan word die oproepdraad aan sy eie toestelle oorgelaat.

In die tweede opsie blokkeer ons die oproepdraad totdat die resultaat van die metode bereken is. Dit is sleg, nie net omdat ons 'n draad, so 'n waardevolle hulpbron van die program, met eenvoudige ledigheid beset het nie, maar ook omdat as die kode van die metode wat ons roep bevat wag, en die sinchronisasiekonteks vereis dat u na die oproepdraad terugkeer na wag, dan sal ons 'n dooiepunt kry: Die oproepdraad wag vir die resultaat van die asinchroniese metode om bereken te word, die asynchrone metode probeer tevergeefs om die uitvoering daarvan in die oproepdraad voort te sit.

Nog 'n nadeel van hierdie benadering is ingewikkelde fouthantering. Die feit is dat foute in asynchrone kode wanneer asynchrone/wag gebruik word baie maklik is om te hanteer - hulle tree dieselfde op asof die kode sinchronies is. Terwyl as ons sinchroniese wag-eksorsisme op 'n taak toepas, verander die oorspronklike uitsondering in 'n AggregateException, d.w.s. Om die uitsondering te hanteer, sal jy die InnerException-tipe moet ondersoek en self 'n if-ketting binne een vangblok moet skryf of die vangs gebruik wanneer konstruk, in plaas van die ketting van vangblokke wat meer bekend is in die C#-wêreld.

Die derde en laaste voorbeelde word ook om dieselfde rede sleg gemerk en bevat dieselfde probleme.

Die WhenAny- en WhenAll-metodes is uiters gerieflik om vir 'n groep take te wag; hulle vou 'n groep take in een, wat óf sal begin wanneer 'n taak van die groep die eerste keer geaktiveer word, óf wanneer almal van hulle hul uitvoering voltooi het.

Stop drade

Om verskeie redes kan dit nodig wees om die vloei te stop nadat dit begin het. Daar is 'n aantal maniere om dit te doen. Die Thread-klas het twee toepaslike benoemde metodes: Aborteer и onderbreking. Die eerste een word sterk nie aanbeveel vir gebruik nie, want nadat dit op enige willekeurige oomblik gebel is, tydens die verwerking van enige instruksie, sal 'n uitsondering gegooi word ThreadAbortedException. Jy verwag nie dat so 'n uitsondering gegooi word wanneer enige heelgetalveranderlike verhoog word nie, reg? En wanneer hierdie metode gebruik word, is dit 'n baie werklike situasie. As jy moet verhoed dat die CLR so 'n uitsondering in 'n sekere gedeelte van die kode genereer, kan jy dit in oproepe omvou Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Enige kode wat in 'n finale blok geskryf is, word in sulke oproepe toegedraai. Om hierdie rede kan jy in die dieptes van die raamwerkkode blokke vind met 'n leë drie, maar nie uiteindelik 'n leë nie. Microsoft ontmoedig hierdie metode so baie dat hulle dit nie by .net core ingesluit het nie.

Die Interrupt-metode werk meer voorspelbaar. Dit kan die draad met 'n uitsondering onderbreek ThreadInterruptedException slegs gedurende daardie oomblikke wanneer die draad in 'n wagtoestand is. Dit gaan in hierdie toestand terwyl dit hang terwyl dit wag vir WaitHandle, slot, of nadat Thread.Sleep gebel is.

Albei opsies wat hierbo beskryf is, is sleg as gevolg van hul onvoorspelbaarheid. Die oplossing is om 'n struktuur te gebruik Kansellasietoken en klas CancellationTokenSource. Die punt is dit: 'n instansie van die CancellationTokenSource-klas word geskep en slegs die een wat dit besit kan die operasie stop deur die metode te roep Kanselleer. Slegs die CancellationToken word na die operasie self oorgedra. CancellationToken-eienaars kan nie self die operasie kanselleer nie, maar kan net kyk of die operasie gekanselleer is. Daar is 'n Boole-eienskap hiervoor Is kansellasieversoek en metode ThrowIfCancelRequested. Laasgenoemde sal 'n uitsondering skep TaakGekanselleer Uitsondering as die Kanselleer-metode opgeroep is op die CancellationToken-instansie wat ge-parrot word. En dit is die metode wat ek aanbeveel om te gebruik. Dit is 'n verbetering teenoor die vorige opsies deur volle beheer te verkry oor wanneer 'n uitsonderingsoperasie gestaak kan word.

Die mees brutale opsie om 'n draad te stop, is om die Win32 API TerminateThread-funksie te noem. Die gedrag van die CLR na die oproep van hierdie funksie kan onvoorspelbaar wees. Op MSDN word die volgende oor hierdie funksie geskryf: "TerminateThread is 'n gevaarlike funksie wat slegs in die mees ekstreme gevalle gebruik moet word. “

Die omskakeling van verouderde API na taakgebaseer met behulp van FromAsync-metode

As jy gelukkig genoeg is om aan 'n projek te werk wat begin is nadat Take ingestel is en opgehou het om stil afgryse vir die meeste ontwikkelaars te veroorsaak, sal jy nie met baie ou API's, beide derdepartye en dié van jou span, te doen hê nie. in die verlede gemartel het. Gelukkig het die .NET Framework-span vir ons gesorg, hoewel die doel dalk was om vir onsself te sorg. Hoe dit ook al sy, .NET het 'n aantal hulpmiddels om kode wat in ou asynchrone programmeringbenaderings geskryf is, pynloos om te skakel na die nuwe een. Een daarvan is die FromAsync-metode van TaskFactory. In die kodevoorbeeld hieronder, verpak ek die ou asinkroniseringsmetodes van die WebRequest-klas in 'n taak deur hierdie metode te gebruik.

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

Dit is net 'n voorbeeld en dit is onwaarskynlik dat jy dit met ingeboude tipes hoef te doen, maar enige ou projek wemel eenvoudig van BeginDoSomething-metodes wat IAsyncResult en EndDoSomething-metodes terugstuur wat dit ontvang.

Skakel verouderde API na Taakgebaseer met behulp van TaskCompletionSource-klas

Nog 'n belangrike hulpmiddel om te oorweeg is die klas TaakVoltooiingBron. Wat funksies, doel en werkingsbeginsel betref, kan dit ietwat herinner aan die RegisterWaitForSingleObject-metode van die ThreadPool-klas, waaroor ek hierbo geskryf het. Deur hierdie klas te gebruik, kan jy ou asynchrone API's maklik en gerieflik in Take toevou.

Jy sal sê dat ek reeds gepraat het oor die FromAsync-metode van die TaskFactory-klas wat vir hierdie doeleindes bedoel is. Hier sal ons die hele geskiedenis van die ontwikkeling van asynchrone modelle in .net moet onthou wat Microsoft oor die afgelope 15 jaar aangebied het: voor die taakgebaseerde asinchrone patroon (TAP), was daar die asynchrone programmeringspatroon (APP), wat was oor metodes BeginDoen iets wat terugkeer IAsyncResult en metodes EindeDoSomething wat dit aanvaar en vir die nalatenskap van hierdie jare is die FromAsync-metode net perfek, maar met verloop van tyd is dit vervang deur die Event Based Asynchronous Pattern (OBP), wat aanvaar het dat 'n gebeurtenis geopper sou word wanneer die asinchroniese bewerking voltooi is.

TaskCompletionSource is perfek om take en verouderde API's wat rondom die gebeurtenismodel gebou is, saam te voeg. Die kern van sy werk is soos volg: 'n objek van hierdie klas het 'n publieke eiendom van die tipe Taak, waarvan die toestand beheer kan word deur die SetResult, SetException, ens. metodes van die TaskCompletionSource klas. Op plekke waar die wag-operateur op hierdie taak toegepas is, sal dit uitgevoer word of misluk met 'n uitsondering, afhangende van die metode wat op die TaakVoltooibron toegepas is. As dit steeds nie duidelik is nie, kom ons kyk na hierdie kodevoorbeeld, waar een of ander ou EAP API in 'n taak toegedraai is deur 'n TaskCompletionSource te gebruik: wanneer die gebeurtenis begin, sal die taak in die Voltooide toestand geplaas word, en die metode wat die wag-operateur toegepas het na hierdie taak sal die uitvoering daarvan hervat nadat die voorwerp ontvang is lei.

public static Task<Result> DoAsync(this SomeApiInstance someApiObj) {

    var completionSource = new TaskCompletionSource<Result>();
    someApiObj.Done += 
        result => completionSource.SetResult(result);
    someApiObj.Do();

    result completionSource.Task;
}

TaakVoltooiingBron Wenke en truuks

Die toedraai van ou API's is nie al wat gedoen kan word met TaskCompletionSource nie. Die gebruik van hierdie klas bied 'n interessante moontlikheid om verskeie API's te ontwerp op take wat nie drade beset nie. En die stroom, soos ons onthou, is 'n duur hulpbron en hul aantal is beperk (hoofsaaklik deur die hoeveelheid RAM). Hierdie beperking kan maklik bereik word deur byvoorbeeld 'n gelaaide webtoepassing met komplekse besigheidslogika te ontwikkel. Kom ons kyk na die moontlikhede waarvan ek praat wanneer ons so 'n truuk soos Long Polling implementeer.

Kortom, die kern van die truuk is dit: jy moet inligting van die API ontvang oor sommige gebeure wat aan sy kant plaasvind, terwyl die API om een ​​of ander rede nie die gebeurtenis kan rapporteer nie, maar net die toestand kan teruggee. 'n Voorbeeld hiervan is alle API's wat bo-op HTTP gebou is voor die tye van WebSocket of wanneer dit om een ​​of ander rede onmoontlik was om hierdie tegnologie te gebruik. Die kliënt kan die HTTP-bediener vra. Die HTTP-bediener kan nie self kommunikasie met die kliënt inisieer nie. 'n Eenvoudige oplossing is om die bediener te poll met 'n timer, maar dit skep bykomende las op die bediener en 'n bykomende vertraging gemiddeld TimerInterval / 2. Om dit te omseil, is 'n truuk genaamd Long Polling uitgevind, wat behels dat die reaksie van die bediener totdat die Timeout verstryk of 'n gebeurtenis sal plaasvind. As die gebeurtenis plaasgevind het, word dit verwerk, indien nie, word die versoek weer gestuur.

while(!eventOccures && !timeoutExceeded)  {

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

Maar so 'n oplossing sal verskriklik blyk te wees sodra die aantal kliënte wat vir die geleentheid wag, toeneem, want ... Elke so 'n kliënt beslaan 'n hele draad wat wag vir 'n gebeurtenis. Ja, en ons kry 'n bykomende vertraging van 1 ms wanneer die gebeurtenis veroorsaak word, meestal is dit nie betekenisvol nie, maar hoekom maak die sagteware erger as wat dit kan wees? As ons Thread.Sleep(1) verwyder, dan sal ons tevergeefs een verwerkerkern 100% ledig laai, wat in 'n nuttelose siklus roteer. Deur TaskCompletionSource te gebruik, kan jy hierdie kode maklik hermaak en al die probleme wat hierbo geïdentifiseer is oplos:

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

Hierdie kode is nie produksiegereed nie, maar net 'n demo. Om dit in werklike gevalle te gebruik, moet u ook ten minste die situasie hanteer wanneer 'n boodskap aankom op 'n tyd wanneer niemand dit verwag nie: in hierdie geval moet die AsseptMessageAsync-metode 'n reeds voltooide taak terugstuur. As dit die mees algemene geval is, kan u daaraan dink om ValueTask te gebruik.

Wanneer ons 'n versoek vir 'n boodskap ontvang, skep en plaas ons 'n TaakVoltooiingsbron in die woordeboek, en wag dan vir wat eerste gebeur: die gespesifiseerde tydinterval verstryk of 'n boodskap word ontvang.

Waardetaak: hoekom en hoe

Die async/wag-operateurs, soos die opbrengsopbrengsoperateur, genereer 'n staatsmasjien vanaf die metode, en dit is die skepping van 'n nuwe objek, wat amper altyd nie belangrik is nie, maar in seldsame gevalle kan dit 'n probleem skep. Hierdie geval kan 'n metode wees wat baie gereeld genoem word, ons praat van tiene en honderde duisende oproepe per sekonde. As so 'n metode op so 'n manier geskryf is dat dit in die meeste gevalle 'n resultaat gee wat alle wagmetodes omseil, dan bied .NET 'n hulpmiddel om dit te optimaliseer - die ValueTask-struktuur. Om dit duidelik te maak, kom ons kyk na 'n voorbeeld van die gebruik daarvan: daar is 'n kas waarna ons baie gereeld gaan. Daar is 'n paar waardes daarin en dan gee ons dit eenvoudig terug; indien nie, gaan ons na 'n stadige IO om dit te kry. Ek wil laasgenoemde asinchroon doen, wat beteken dat die hele metode asinchroon blyk te wees. Die voor die hand liggende manier om die metode te skryf is dus soos volg:

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

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

As gevolg van die begeerte om 'n bietjie te optimaliseer, en 'n effense vrees vir wat Roslyn sal genereer wanneer hierdie kode saamgestel word, kan jy hierdie voorbeeld soos volg herskryf:

public Task<string> GetById(int id) {

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

Inderdaad, die optimale oplossing in hierdie geval sal wees om die hot-path te optimaliseer, naamlik om 'n waarde uit die woordeboek te verkry sonder enige onnodige toekennings en las op die GC, terwyl ons in daardie seldsame gevalle nog steeds na IO moet gaan vir data , alles sal 'n plus / minus op die ou manier bly:

public ValueTask<string> GetById(int id) {

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

Kom ons kyk van naderby na hierdie stukkie kode: as daar 'n waarde in die kas is, skep ons 'n struktuur, anders sal die werklike taak in 'n betekenisvolle een toegedraai wees. Die oproepkode gee nie om in watter pad hierdie kode uitgevoer is nie: ValueTask, vanuit 'n C#-sintaksisoogpunt, sal in hierdie geval dieselfde optree as 'n gewone Taak.

Taakskeduleerders: bestuur van taakbekendstellingstrategieë

Die volgende API wat ek graag wil oorweeg, is die klas Taak skeduleerder en sy afgeleides. Ek het reeds hierbo genoem dat TPL die vermoë het om strategieë te bestuur vir die verspreiding van take oor drade. Sulke strategieë word gedefinieer in die afstammelinge van die TaskScheduler-klas. Byna enige strategie wat jy dalk nodig het, kan in die biblioteek gevind word. ParallelExtensions Ekstras, ontwikkel deur Microsoft, maar nie deel van .NET nie, maar verskaf as 'n Nuget-pakket. Kom ons kyk kortliks na sommige van hulle:

  • CurrentThread Task Scheduler - voer take op die huidige draad uit
  • Beperkte gelyktydige vlak Taakskeduleerder - beperk die aantal take wat gelyktydig uitgevoer word deur parameter N, wat in die konstruktor aanvaar word
  • Ordered Task Scheduler - word gedefinieer as LimitedConcurrencyLevelTaskScheduler(1), dus take sal opeenvolgend uitgevoer word.
  • WorkStealingTask Scheduler - implemente werk-diefstal benadering tot taakverspreiding. In wese is dit 'n aparte ThreadPool. Los die probleem op dat in .NET ThreadPool 'n statiese klas is, een vir alle toepassings, wat beteken dat die oorlading of verkeerde gebruik daarvan in een deel van die program tot newe-effekte in 'n ander kan lei. Boonop is dit uiters moeilik om die oorsaak van sulke gebreke te verstaan. Daardie. Daar mag 'n behoefte wees om aparte WorkStealingTaskSchedulers te gebruik in dele van die program waar die gebruik van ThreadPool aggressief en onvoorspelbaar kan wees.
  • Queued Task Scheduler - laat jou toe om take uit te voer volgens reëls vir prioriteitsrye
  • ThreadPerTask Scheduler - skep 'n aparte draad vir elke taak wat daarop uitgevoer word. Kan nuttig wees vir take wat 'n onvoorspelbare lang tyd neem om te voltooi.

Daar is 'n goeie gedetailleerde статья oor TaskSchedulers op die Microsoft-blog.

Vir gerieflike ontfouting van alles wat met Take verband hou, het Visual Studio 'n Take-venster. In hierdie venster kan jy die huidige toestand van die taak sien en spring na die kodereël wat tans uitgevoer word.

.NET: Gereedskap om met multithreading en asinchronie te werk. Deel 1

PLinq en die Parallelle klas

Benewens Take en alles wat daaroor gesê word, is daar nog twee interessante hulpmiddels in .NET: PLinq (Linq2Parallel) en die Parallel-klas. Die eerste beloof parallelle uitvoering van alle Linq-bewerkings op verskeie drade. Die aantal drade kan gekonfigureer word deur die WithDegreeOfParallelism-uitbreidingsmetode te gebruik. Ongelukkig het PLinq meestal in sy verstekmodus nie genoeg inligting oor die interne van jou databron om 'n aansienlike spoedwins te bied nie, aan die ander kant is die koste om te probeer baie laag: jy hoef net die AsParallel-metode te bel voordat die ketting van Linq-metodes en voer prestasietoetse uit. Boonop is dit moontlik om bykomende inligting aan PLinq oor die aard van jou databron deur te gee deur die Partitions-meganisme te gebruik. Jy kan meer lees hier и hier.

Die Parallelle statiese klas bied metodes vir iterasie deur 'n Foreach-versameling in parallel, die uitvoering van 'n For-lus, en die uitvoering van verskeie afgevaardigdes in parallel Invoke. Die uitvoering van die huidige draad sal gestop word totdat die berekeninge voltooi is. Die aantal drade kan gekonfigureer word deur ParallelOptions as die laaste argument deur te gee. U kan ook TaskScheduler en CancellationToken spesifiseer met behulp van opsies.

Bevindinge

Toe ek hierdie artikel begin skryf het op grond van die materiaal van my verslag en die inligting wat ek tydens my werk daarna ingesamel het, het ek nie verwag dat daar soveel daarvan sou wees nie. Nou, wanneer die teksredigeerder waarin ek hierdie artikel tik verwytend vir my sê dat bladsy 15 weg is, sal ek die tussentydse resultate opsom. Ander truuks, API's, visuele gereedskap en slaggate sal in die volgende artikel behandel word.

Gevolgtrekkings:

  • Jy moet die gereedskap ken om met drade, asinchronie en parallelisme te werk om die hulpbronne van moderne rekenaars te gebruik.
  • .NET het baie verskillende gereedskap vir hierdie doeleindes
  • Nie almal van hulle het gelyktydig verskyn nie, so jy kan dikwels erfenis vind, maar daar is maniere om ou API's om te skakel sonder veel moeite.
  • Werk met drade in .NET word verteenwoordig deur die Thread- en ThreadPool-klasse
  • Die Thread.Abort-, Thread.Interrupt- en Win32 API TerminateThread-metodes is gevaarlik en word nie aanbeveel vir gebruik nie. In plaas daarvan is dit beter om die CancellationToken-meganisme te gebruik
  • Vloei is 'n waardevolle hulpbron en die aanbod daarvan is beperk. Situasies waar drade besig is om vir gebeurtenisse te wag, moet vermy word. Hiervoor is dit gerieflik om die TaskCompletionSource-klas te gebruik
  • Die kragtigste en mees gevorderde .NET-instrumente om met parallelisme en asinchronie te werk, is Take.
  • Die c# async/wag-operateurs implementeer die konsep van nie-blokkerende wag
  • Jy kan die verspreiding van take oor drade beheer met behulp van TaskScheduler-afgeleide klasse
  • Die ValueTask-struktuur kan nuttig wees om warm paaie en geheueverkeer te optimaliseer
  • Visual Studio se Take en Threads-vensters verskaf baie nuttige inligting vir die ontfouting van multi-threaded of asynchrone kode
  • PLinq is 'n oulike hulpmiddel, maar dit het dalk nie genoeg inligting oor jou databron nie, maar dit kan reggestel word met behulp van die partisiemeganisme
  • Vervolg…

Bron: will.com

Voeg 'n opmerking