.NET: Tools foar wurkjen mei multithreading en asynchrony. Diel 1

Ik publisearje it orizjinele artikel oer Habr, wêrfan de oersetting is pleatst yn it bedriuw blogpost.

De needsaak om wat asynchronysk te dwaan, sûnder hjir en no te wachtsjen op it resultaat, of om grut wurk te ferdielen ûnder ferskate ienheden dy't it útfiere, bestie foar de komst fan kompjûters. Mei harren komst waard dizze need tige taastber. No, yn 2019, typ ik dit artikel op in laptop mei in 8-kearn Intel Core-prosessor, wêrop mear dan hûndert prosessen parallel rinne, en noch mear threads. Tichtby is d'r in wat sjofele tillefoan, kocht in pear jier lyn, it hat in 8-kearnprosessor oan board. Tematyske boarnen binne fol mei artikels en fideo's wêr't har auteurs de flaggeskipsmartphones fan dit jier bewûnderje dy't 16-kearnprozessors hawwe. MS Azure leveret in firtuele masine mei in 20-kearnprosessor en 128 TB RAM foar minder dan $ 2 / oere. Spitigernôch is it ûnmooglik om it maksimum út te heljen en dizze krêft te benutten sûnder de ynteraksje fan triedden te behearjen.

Terminology

Proses - OS-objekt, isolearre adresromte, befettet triedden.
Thread - in OS-objekt, de lytste ienheid fan útfiering, diel fan in proses, threads diele ûnthâld en oare boarnen ûnderinoar binnen in proses.
Multitasking - OS-eigendom, de mooglikheid om ferskate prosessen tagelyk út te fieren
Multi-core - in eigendom fan 'e prosessor, de mooglikheid om ferskate kearnen te brûken foar gegevensferwurking
Multiferwurking - in eigendom fan in kompjûter, de mooglikheid om tagelyk fysyk te wurkjen mei ferskate processors
Multithreading - in eigenskip fan in proses, de mooglikheid om gegevensferwurking te fersprieden oer ferskate diskusjes.
Parallelisme - it útfieren fan ferskate aksjes fysyk tagelyk per ienheid fan tiid
Asynchrony - útfiering fan in operaasje sûnder te wachtsjen op it foltôgjen fan dizze ferwurking; it resultaat fan 'e útfiering kin letter ferwurke wurde.

Metafoar

Net alle definysjes binne goed en guon hawwe ekstra útlis nedich, dus ik sil in metafoar tafoegje oer it koken fan moarnsiten oan 'e formeel yntrodusearre terminology. Koken moarnsbrochje yn dizze metafoar is in proses.

By it tarieden fan moarnsbrochje yn 'e moarn ik (CPU) Ik kom nei de keuken (Computer). ik haw 2 hannen (cores). D'r binne in oantal apparaten yn 'e keuken (IO): oven, tsjettel, broodrooster, kuolkast. Ik set it gas oan, set der in frettenpanne op en giet oalje deryn sûnder te wachtsjen oant it opwarmt (asynchronously, Non-Blocking-IO-Wachtsje), Ik nim de aaien út 'e kuolkast en brek se yn in plaat, slach se dan mei ien hân (Diskusje #1), en twadde (Diskusje #2) holding de plaat (Shared Resource). No wol ik de tsjettel oansette, mar ik haw net genôch hannen (Thread Starvation) Yn dizze tiid wurdt de frettenpanne ferwaarme (It resultaat ferwurkjen) wêryn ik wat ik swaaid haw yngie. Ik gryp nei de tsjettel en set dy oan en sjoch stom it wetter deryn siede (Blocking-IO-Wachtsje), hoewol hy yn dizze tiid de plaat koe wosken hawwe wêr't hy de omelet sloech.

Ik kocht in omelet mei mar 2 hannen, en ik haw net mear, mar tagelyk, op it momint fan it slaan fan 'e omelet, fûnen 3 operaasjes tagelyk: De CPU is it fluchste diel fan 'e kompjûter, IO is wat meastal alles fertraget, sa faak is in effektive oplossing om de CPU mei wat te besetten by it ûntfangen fan gegevens fan IO.

Trochgean mei de metafoar:

  • As ik yn it proses fan it tarieden fan in omelet ek besykje om klean te feroarjen, soe dit in foarbyld wêze fan multitasking. In wichtige nuânse: kompjûters binne dêr folle better yn as minsken.
  • In keuken mei ferskate chefs, bygelyks yn in restaurant - in multi-core kompjûter.
  • In protte restaurants yn in food rjochtbank yn in winkelsintrum - datacenter

.NET Tools

.NET is goed yn it wurkjen mei triedden, lykas mei in protte oare dingen. Mei elke nije ferzje yntroduseart it mear en mear nije ark om mei har te wurkjen, nije lagen fan abstraksje oer OS-threads. By it wurkjen mei de konstruksje fan abstraksjes brûke kaderûntwikkelders in oanpak dy't de kâns lit, by it brûken fan in abstraksje op heech nivo, ien of mear nivo's hjirûnder del te gean. Meastentiids is dit net nedich, yn feite iepenet it de doar om josels yn 'e foet te sjitten mei in jachtgewear, mar soms, yn seldsume gefallen, kin it de ienige manier wêze om in probleem op te lossen dat net is oplost op it hjoeddeistige nivo fan abstraksje .

Mei ark bedoel ik sawol applikaasje-programmearring-ynterfaces (API's) levere troch it ramt en pakketten fan tredden, lykas heule software-oplossings dy't it sykjen nei problemen ferienfâldigje yn ferbân mei multi-threaded koade.

In tried begjinne

De Thread-klasse is de meast basale klasse yn .NET foar it wurkjen mei threads. De konstruktor akseptearret ien fan twa ôffurdigen:

  • ThreadStart - Gjin parameters
  • ParametrizedThreadStart - mei ien parameter fan type objekt.

De ôffurdige sil útfierd wurde yn de nij oanmakke thread nei it oproppen fan de Startmetoade. As in delegate fan it type ParametrizedThreadStart trochjûn is oan de konstruktor, dan moat in objekt trochjûn wurde oan de Startmetoade. Dit meganisme is nedich om alle lokale ynformaasje oer te bringen nei de stream. It is de muoite wurdich opskriuwen dat it meitsjen fan in tried is in djoere operaasje, en de tried sels is in swier foarwerp, teminsten omdat it allocates 1MB ûnthâld op 'e stack en fereasket ynteraksje mei de OS API.

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

De ThreadPool-klasse stiet foar it konsept fan in swimbad. Yn .NET is de threadpool in stik yngenieur, en de ûntwikkelders by Microsoft hawwe in protte muoite dien om te soargjen dat it optimaal wurket yn in grut ferskaat oan senario's.

Algemien konsept:

Fan it momint dat de applikaasje begjint, makket it ferskate threads yn reserve op 'e eftergrûn en biedt de mooglikheid om se foar gebrûk te nimmen. As threads faak en yn grutte oantallen wurde brûkt, wreidet it swimbad út om te foldwaan oan 'e behoeften fan' e beller. As d'r op it krekte momint gjin frije triedden yn it swimbad binne, sil it of wachtsje op ien fan 'e triedden om werom te kommen, of in nije oanmeitsje. It folget dat de threadpool geweldig is foar guon aksjes op koarte termyn en min geskikt foar operaasjes dy't rinne as tsjinsten yn 'e heule operaasje fan' e applikaasje.

Foar in gebrûk in tried út it swimbad, der is in QueueUserWorkItem metoade dy't akseptearret in ôffurdige fan type WaitCallback, dat hat deselde hântekening as ParametrizedThreadStart, en de parameter trochjûn oan it fiert deselde funksje.

ThreadPool.QueueUserWorkItem(...);

De minder bekende metoade fan threadpool RegisterWaitForSingleObject wurdt brûkt om net-blokkearjende IO-operaasjes te organisearjen. De ôffurdige trochjûn oan dizze metoade sil oproppen wurde as de WaitHandle dy't trochjûn is oan 'e metoade "Released" is.

ThreadPool.RegisterWaitForSingleObject(...)

.NET hat in thread timer en it ferskilt fan WinForms / WPF timers yn dat syn handler wurdt neamd op in tried nommen út it swimbad.

System.Threading.Timer

D'r is ek in frij eksoatyske manier om in ôffurdige te stjoeren foar útfiering nei in thread út it swimbad - de BeginInvoke-metoade.

DelegateInstance.BeginInvoke

Ik soe graach koart wenje op 'e funksje dêr't in protte fan' e boppesteande metoaden kinne wurde neamd - CreateThread fan Kernel32.dll Win32 API. D'r is in manier, tank oan it meganisme fan eksterne metoaden, om dizze funksje te neamen. Sa'n oprop haw ik mar ien kear sjoen yn in ferskriklik foarbyld fan legacy-koade, en de motivaasje fan de skriuwer dy't dat krekt dien hat, bliuwt my noch in riedsel.

Kernel32.dll CreateThread

Threads besjen en debuggen

Threads makke troch jo, alle komponinten fan tredden, en de .NET-pool kinne wurde besjoen yn it Threads-finster fan Visual Studio. Dit finster sil allinich threadynformaasje werjaan as de applikaasje ûnder debug is en yn Break-modus is. Hjir kinne jo maklik de stacknammen en prioriteiten fan elke thread besjen, en debuggen wikselje nei in spesifike thread. Mei de Priority-eigenskip fan 'e Thread-klasse kinne jo de prioriteit fan in thread ynstelle, dy't de OC en CLR sille waarnimme as in oanbefelling by it dielen fan prosessortiid tusken threads.

.NET: Tools foar wurkjen mei multithreading en asynchrony. Diel 1

Taak Parallel Library

Task Parallel Library (TPL) waard yntrodusearre yn .NET 4.0. No is it de standert en it wichtichste ark foar wurkjen mei asynchrony. Elke koade dy't in âldere oanpak brûkt wurdt beskôge as legacy. De basisienheid fan TPL is de Task-klasse fan 'e System.Threading.Tasks-nammeromte. In taak is in abstraksje oer in tried. Mei de nije ferzje fan 'e C#-taal hawwe wy in elegante manier krigen om te wurkjen mei Taken - async/wachtoperators. Dizze konsepten makken it mooglik om asynchrone koade te skriuwen as wie it ienfâldich en syngroan, dit makke it mooglik foar minsken mei in bytsje begryp fan 'e ynterne wurking fan threads om applikaasjes te skriuwen dy't se brûke, applikaasjes dy't net befrieze by it útfieren fan lange operaasjes. It brûken fan async / wachtsje is in ûnderwerp foar ien of sels meardere artikels, mar ik sil besykje de essinsje derfan yn in pear sinnen te krijen:

  • async is in modifier fan in metoade dy't Task of void werombringt
  • en wachtsje is in net-blokkearjende Taak wachtsjende operator.

Nochris: de wachtoperator, yn it algemiene gefal (der binne útsûnderingen), sil de hjoeddeistige thread fan útfiering fierder loslitte, en as de Task syn útfiering einiget, en de thread (yn feite soe it krekter wêze om de kontekst te sizzen , mar dêroer letter mear) sil de metoade fierder útfiere. Binnen .NET, dit meganisme wurdt útfierd op deselde wize as opbringst werom, doe't de skreaune metoade feroaret yn in hiele klasse, dat is in steat masine en kin útfierd wurde yn aparte stikken ôfhinklik fan dizze steaten. Elkenien dy't ynteressearre is kin elke ienfâldige koade skriuwe mei asynс / wachtsje, de gearstalling kompilearje en besjen mei JetBrains dotPeek mei Compiler Generated Code ynskeakele.

Litte wy nei opsjes sjen foar it starten en brûken fan in taak. Yn it koade foarbyld hjirûnder meitsje wy in nije taak dy't neat nuttich docht (Thread.Sleep (10000)), mar yn it echte libben soe dit wat kompleks CPU-yntinsyf wurk wêze moatte.

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
}

In taak wurdt makke mei in oantal opsjes:

  • LongRunning is in oanwizing dat de taak net fluch sil wurde foltôge, wat betsjut dat it kin wêze wurdich beskôgje net nimme in tried út it swimbad, mar it meitsjen fan in aparte ien foar dizze taak om net te kwea oaren.
  • AttachedToParent - Taken kinne wurde regele yn in hiërargy. As dizze opsje waard brûkt, dan kin de taak wêze yn in steat dêr't it sels is foltôge en wachtet op de útfiering fan syn bern.
  • PreferFairness - betsjut dat it better soe wêze om taken út te fieren dy't stjoerd binne foar útfiering earder foardat dy letter ferstjoerd binne. Mar dit is gewoan in oanbefelling en resultaten wurde net garandearre.

De twadde parameter trochjûn oan de metoade is CancellationToken. Om it annulearjen fan in operaasje korrekt te behanneljen nei't it is begon, moat de koade dy't wurdt útfierd wurde fol mei kontrôles foar de CancellationToken-status. As d'r gjin kontrôles binne, dan sil de Ofbrekke-metoade neamd op it CancellationTokenSource-objekt de útfiering fan 'e taak allinich kinne stopje foardat it begjint.

De lêste parameter is in plannerobjekt fan it type TaskScheduler. Dizze klasse en syn neiteam binne ûntworpen om strategyen te behearjen foar it fersprieden fan Taken oer diskusjes; standert sil de Taak wurde útfierd op in willekeurige thread út it swimbad.

De wachtoperator wurdt tapast op de oanmakke taak, wat betsjut dat de koade dy't dernei skreaun is, as d'r ien is, wurdt útfierd yn deselde kontekst (faak betsjut dit op deselde thread) as de koade foarôf wachtsjen.

De metoade is markearre as async void, wat betsjut dat it kin brûke de wacht operator, mar de oprop koade sil net by steat wêze om te wachtsjen op útfiering. As sa'n funksje nedich is, dan moat de metoade Task werombringe. Metoaden markearre async void binne hiel gewoan: as in regel, dit binne evenemint handlers of oare metoaden dy't wurkje op it fjoer en ferjit prinsipe. As jo ​​​​net allinich de kâns moatte jaan om te wachtsjen oant it ein fan 'e útfiering, mar ek it resultaat weromjaan, dan moatte jo Task brûke.

Op de taak dy't de StartNew-metoade weromkaam, lykas op elke oare, kinne jo de ConfigureAwait-metoade neame mei de falske parameter, dan sil de útfiering nei it wachtsjen trochgean net op 'e finzene kontekst, mar op in willekeurige. Dit moat altyd dien wurde as de útfieringskontekst is net wichtich foar de koade nei ôfwachtsjen. Dit is ek in oanbefelling fan MS by it skriuwen fan koade dy't ferpakt wurdt levere yn in bibleteek.

Litte wy wat mear dwaen oer hoe't jo kinne wachtsje op it foltôgjen fan in taak. Hjirûnder is in foarbyld fan koade, mei opmerkings oer wannear't de ferwachting betingst goed dien wurdt en wannear't it betingst min dien wurdt.

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
}

Yn it earste foarbyld wachtsje wy op 'e taak om te foltôgjen sûnder de oproptried te blokkearjen; wy sille allinich weromgean nei it ferwurkjen fan it resultaat as it der al is; oant dan wurdt de oproptried oerlitten oan har eigen apparaten.

Yn 'e twadde opsje blokkearje wy de oproptried oant it resultaat fan' e metoade wurdt berekkene. Dit is min net allinich om't wy in thread hawwe beset, sa'n weardefolle boarne fan it programma, mei ienfâldige idleness, mar ek om't as de koade fan 'e metoade dy't wy neame befette wachtsje, en de syngronisaasjekontekst fereasket werom te gean nei de oproptried nei wachtsje, dan krije wy in deadlock: De oproptried wachtet op it resultaat fan 'e asynchrone metoade om te berekkenjen, de asynchrone metoade besiket om 'e nocht syn útfiering yn' e oproptried troch te gean.

In oar neidiel fan dizze oanpak is yngewikkelde flaterôfhanneling. It feit is dat flaters yn asynchrone koade by it brûken fan asyngroane / wachtsjen binne hiel maklik te behanneljen - se gedrage itselde as soe de koade syngroane. Wylst as wy syngroane wachteksorsisme tapasse op in taak, feroaret de oarspronklike útsûndering yn in AggregateException, d.w.s. Om de útsûndering te behanneljen, moatte jo it InnerException-type ûndersykje en josels in as ketting skriuwe binnen ien fangenblok of it fangen brûke by it bouwen, ynstee fan 'e keatling fan fangblokken dy't mear bekend is yn' e C # wrâld.

De tredde en lêste foarbylden binne ek markearre min foar deselde reden en befetsje allegearre deselde problemen.

De metoaden WhenAny en WhenAll binne ekstreem handich om te wachtsjen op in groep Taken; se pakke in groep Taken yn ien, dy't sil ûntsteane as in Task fan 'e groep foar it earst wurdt aktivearre, of as se allegear har útfiering foltôge hawwe.

Stopje triedden

Om ferskate redenen kin it nedich wêze om de stream te stopjen nei't it begon is. D'r binne in oantal manieren om dit te dwaan. De Thread-klasse hat twa passend neamde metoaden: Ofbrekke и Ûnderbrekke. De earste is tige net oan te rieden foar gebrûk, om't nei it oproppen fan it op elts willekeurig momint, tidens de ferwurking fan elke ynstruksje, sil in útsûndering wurde smiten ThreadAbortedException. Jo ferwachtsje net dat sa'n útsûndering sil wurde smiten as jo in heule getal fariabele ferheegje, toch? En by it brûken fan dizze metoade is dit in heul echte situaasje. As jo ​​​​foarkomme moatte dat de CLR sa'n útsûndering generearret yn in bepaalde seksje fan koade, kinne jo it yn oproppen ferpakke Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Elke koade skreaun yn in einlingsblok wurdt ferpakt yn sokke oproppen. Om dizze reden kinne jo yn 'e djipten fan' e ramtkoade blokken fine mei in lege besykjen, mar net in lege einlings. Microsoft ûntmoediget dizze metoade safolle dat se it net opnommen hawwe yn .net-kearn.

De Interrupt-metoade wurket mear foarsisber. It kin de tried ûnderbrekke mei in útsûndering ThreadInterruptedException allinich yn dy mominten dat de tried yn in wachtstân is. It komt yn dizze steat wylst hingjen wylst wachtsje op WaitHandle, slot, of nei't oproppen Thread.Sleep.

Beide opsjes beskreaun hjirboppe binne min fanwege harren ûnfoarspelberens. De oplossing is om in struktuer te brûken CancellationToken en klasse CancellationTokenSource. It punt is dit: in eksimplaar fan 'e klasse CancellationTokenSource wurdt makke en allinich dejinge dy't it hat kin de operaasje stopje troch de metoade op te roppen Ofbrekke. Allinich de CancellationToken wurdt trochjûn oan de operaasje sels. CancellationToken-eigners kinne de operaasje sels net annulearje, mar kinne allinich kontrolearje oft de operaasje annulearre is. D'r is in Booleaanske eigendom foar dit IsCancellationRequested en metoade ThrowIfCancelRequested. Dy lêste sil smyt in útsûndering TaskCancelledException as de Ofbrekke metoade waard oanroppen op de CancellationToken eksimplaar wurdt parroted. En dit is de metoade dy't ik advisearje te brûken. Dit is in ferbettering oer de foarige opsjes troch it krijen fan folsleine kontrôle oer op hokker punt in útsûnderingsoperaasje kin wurde ôfbrutsen.

De meast brutale opsje foar it stopjen fan in thread is de Win32 API TerminateThread-funksje te neamen. It gedrach fan 'e CLR nei it oproppen fan dizze funksje kin ûnfoarspelber wêze. Op MSDN wurdt it folgjende skreaun oer dizze funksje: "TerminateThread is in gefaarlike funksje dy't allinich brûkt wurde moat yn 'e meast ekstreme gefallen. "

Konvertearje legacy API nei taak basearre mei FromAsync metoade

As jo ​​​​gelok hawwe om te wurkjen oan in projekt dat waard begon nei't Tasks waarden yntrodusearre en ophâlde stille horror te feroarsaakjen foar de measte ûntwikkelders, dan hoege jo net te meitsjen mei in protte âlde API's, sawol tredden as dy fan jo team hat martele yn it ferline. Lokkich soarge it .NET Framework-team foar ús, hoewol it doel miskien wie om foar ússels te soargjen. Hoe dan ek, .NET hat in oantal ark foar it pynlik konvertearjen fan koade skreaun yn âlde asynchrone programmearring oanpakken nei de nije. Ien fan har is de FromAsync-metoade fan TaskFactory. Yn it koade-foarbyld hjirûnder wrap ik de âlde async-metoaden fan 'e WebRequest-klasse yn in taak mei dizze metoade.

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

Dit is gewoan in foarbyld en it is net wierskynlik dat jo dit moatte dwaan mei ynboude typen, mar elk âld projekt is gewoan fol mei BeginDoSomething-metoaden dy't IAsyncResult- en EndDoSomething-metoaden werombringe dy't it ûntfange.

Konvertearje legacy API nei Task Based mei TaskCompletionSource klasse

In oar wichtich ark om te beskôgjen is de klasse TaskCompletionSource. Wat funksjes, doel en prinsipe fan wurking oanbelanget, kin it wat tinke oan 'e RegisterWaitForSingleObject-metoade fan' e ThreadPool-klasse, dêr't ik hjirboppe oer skreau. Mei dizze klasse kinne jo âlde asynchrone API's maklik en maklik yn Tasks ynpakke.

Jo sille sizze dat ik al praat oer de FromAsync-metoade fan 'e TaskFactory-klasse bedoeld foar dizze doelen. Hjir sille wy de hiele skiednis fan 'e ûntwikkeling fan asynchrone modellen yn .net moatte ûnthâlde dy't Microsoft yn' e ôfrûne 15 jier oanbean hat: foar it Task-Based Asynchronous Pattern (TAP), wie d'r it Asynchronous Programming Pattern (APP), dat gie oer metoaden BegjinneDoSomething werom IAsyncResult en metoaden EinDoSomething dat it akseptearret en foar de neilittenskip fan dizze jierren is de FromAsync-metoade gewoan perfekt, mar mei de tiid waard it ferfongen troch it Event Based Asynchronous Pattern (EN AP), dy't oannommen dat in evenemint soe wurde opheft as de asynchrone operaasje foltôge.

TaskCompletionSource is perfekt foar it ynpakken fan Taken en legacy API's boud om it evenemintmodel. De essinsje fan syn wurk is as folget: in objekt fan dizze klasse hat in iepenbiere eigendom fan it type Task, wêrfan de steat kin wurde regele troch de metoaden SetResult, SetException, ensfh. fan 'e klasse TaskCompletionSource. Op plakken dêr't de wachtoperator waard tapast op dizze taak, sil it wurde útfierd of mislearre mei in útsûndering ôfhinklik fan de metoade tapast op de TaskCompletionSource. As it noch net dúdlik is, litte wy nei dit koadefoarbyld sjen, wêr't guon âlde EAP API is ferpakt yn in taak mei in TaskCompletionSource: as it evenemint ûntspringt, sil de taak wurde oerbrocht nei de foltôge steat, en de metoade dy't de wachtoperator tapast nei dizze Taak sil syn útfiering ferfetsje nei it ûntfangen fan it objekt resultaat.

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 Tips & Tricks

It ynpakken fan âlde API's is net alles dat kin wurde dien mei TaskCompletionSource. It brûken fan dizze klasse iepenet in nijsgjirrige mooglikheid om ferskate API's te ûntwerpen op Taken dy't net wurde beset troch threads. En de stream, lykas wy ûnthâlde, is in djoere boarne en har oantal is beheind (benammen troch it bedrach fan RAM). Dizze beheining kin maklik berikt wurde troch it ûntwikkeljen fan, bygelyks, in laden webapplikaasje mei komplekse saaklike logika. Litte wy de mooglikheden beskôgje wêr't ik it oer ha by it útfieren fan sa'n trúk as Long-Polling.

Koartsein, de essinsje fan 'e trúk is dit: jo moatte ynformaasje krije fan' e API oer guon eveneminten dy't op har kant foarkomme, wylst de API, om ien of oare reden, it barren net kin rapportearje, mar allinich de steat weromjaan kin. In foarbyld hjirfan binne alle API's dy't boppe op HTTP boud binne foar de tiden fan WebSocket of as it om ien of oare reden ûnmooglik wie om dizze technology te brûken. De kliïnt kin de HTTP-tsjinner freegje. De HTTP-tsjinner kin net sels kommunikaasje mei de kliïnt begjinne. In ienfâldige oplossing is in poll de tsjinner mei help fan in timer, mar dit soarget foar in ekstra lading op de tsjinner en in ekstra fertraging yn trochsneed TimerInterval / 2. Om om te kommen dit, in trúk neamd Long Polling waard útfûn, dat giet it om fertraging fan de reaksje fan de tsjinner oant de Timeout ferrint of in evenemint sil plakfine. As it evenemint bard is, dan wurdt it ferwurke, sa net, dan wurdt it fersyk wer ferstjoerd.

while(!eventOccures && !timeoutExceeded)  {

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

Mar sa'n oplossing sil ferskriklik blike te wêzen sa gau as it oantal kliïnten dat op it evenemint wachtet tanimt, om't ... Elke sa'n kliïnt beslacht in heule thread dy't wachtet op in evenemint. Ja, en wy krije in ekstra fertraging fan 1 ms as it barren wurdt trigger, meastentiids is dit net wichtich, mar wêrom meitsje de software slimmer dan it kin wêze? As wy fuortsmite Thread.Sleep (1), dan om 'e nocht wy sille lade ien prosessor kearn 100% idle, draaiende yn in nutteleaze syklus. Mei TaskCompletionSource kinne jo dizze koade maklik opnij meitsje en alle hjirboppe identifisearre problemen oplosse:

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

Dizze koade is net produksje-klear, mar gewoan in demo. Om it yn echte gefallen te brûken, moatte jo ek op syn minst de situaasje behannelje as in berjocht komt op in tiid dat gjinien it ferwachtet: yn dit gefal moat de AsseptMessageAsync-metoade in al foltôge Task werombringe. As dit it meast foarkommende gefal is, dan kinne jo tinke oan it brûken fan ValueTask.

As wy in fersyk foar in berjocht ûntfange, meitsje en pleatse wy in TaskCompletionSource yn it wurdboek, en wachtsje dan op wat der earst bart: it opjûne tiidynterval ferrint of in berjocht wurdt ûntfongen.

ValueTask: wêrom en hoe

De async / wachtsje operators, lykas de opbringst werom operator, generearje in steat masine út de metoade, en dit is it meitsjen fan in nij foarwerp, dat is hast altyd net wichtich, mar yn seldsume gefallen kin meitsje in probleem. Dit gefal kin in metoade wêze dy't echt faak neamd wurdt, wy prate oer tsientallen en hûnderttûzenen oproppen per sekonde. As sa'n metoade op sa'n manier skreaun is dat it yn 'e measte gefallen in resultaat jout dy't alle wachtmetoaden omgiet, dan leveret .NET in ark om dit te optimalisearjen - de ValueTask-struktuer. Om it dúdlik te meitsjen, litte wy nei in foarbyld sjen fan it gebrûk: d'r is in cache wêr't wy heul faak nei gean. D'r binne wat wearden yn en dan jouwe wy se gewoan werom; sa net, dan geane wy ​​nei wat stadige IO om se te krijen. Ik wol dat lêste asynchronous dwaan, wat betsjut dat de heule metoade asynchronous blykt te wêzen. Sa is de foar de hân lizzende manier om de metoade te skriuwen as folget:

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

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

Fanwegen de winsk om in bytsje te optimalisearjen, en in lichte eangst foar wat Roslyn sil generearje by it kompilearjen fan dizze koade, kinne jo dit foarbyld as folgjend oerskriuwe:

public Task<string> GetById(int id) {

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

Yndied, de optimale oplossing yn dit gefal soe wêze om it hot-paad te optimalisearjen, nammentlik in wearde te krijen út it wurdboek sûnder ûnnedige allocaasjes en laden op 'e GC, wylst wy yn dy seldsume gefallen noch moatte gean nei IO foar gegevens , alles bliuwt in plus / minus de âlde manier:

public ValueTask<string> GetById(int id) {

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

Litte wy dit stikje koade tichterby besjen: as d'r in wearde yn 'e cache is, meitsje wy in struktuer, oars sil de echte taak yn in sinfol ferpakt wurde. De oprop koade makket neat út hokker paad dizze koade waard útfierd yn: ValueTask, út in C # syntaksis eachpunt, gedrage itselde as in gewoane Task yn dit gefal.

TaskSchedulers: Behear fan taaklansearringstrategyen

De folgjende API dy't ik graach beskôgje is de klasse TaskScheduler en syn derivaten. Ik haw hjirboppe al neamd dat TPL de mooglikheid hat om strategyen te behearjen foar it fersprieden fan Taken oer diskusjes. Sokke strategyen wurde definiearre yn 'e neikommelingen fan' e TaskScheduler-klasse. Hast elke strategy dy't jo miskien nedich binne kinne fûn wurde yn 'e bibleteek. ParallelExtensionsExtras, ûntwikkele troch Microsoft, mar gjin diel fan .NET, mar levere as in Nuget-pakket. Litte wy koart nei guon fan harren sjen:

  • CurrentThreadTaskScheduler - fiert taken út op 'e hjoeddeistige thread
  • LimitedConcurrencyLevelTaskScheduler - beheint it oantal taken tagelyk útfierd troch parameter N, dy't wurdt akseptearre yn 'e konstruktor
  • OrderedTaskScheduler - wurdt definiearre as LimitedConcurrencyLevelTaskScheduler (1), dus taken sille sequentially wurde útfierd.
  • WorkStealingTaskScheduler - ympleminten wurk-stellerij oanpak fan taakferdieling. Yn essinsje is it in aparte ThreadPool. Lost it probleem op dat yn .NET ThreadPool in statyske klasse is, ien foar alle applikaasjes, wat betsjut dat it oerladen of ferkeard gebrûk yn ien diel fan it programma kin liede ta side-effekten yn in oar. Boppedat is it ekstreem lestich om de oarsaak fan sokke defekten te begripen. Dat. D'r kin in need wêze om aparte WorkStealingTaskSchedulers te brûken yn dielen fan it programma wêr't it gebrûk fan ThreadPool agressyf en ûnfoarspelber kin wêze.
  • QueuedTaskScheduler - kinne jo taken útfiere neffens regels foar prioriteitswachtrige
  • ThreadPerTaskScheduler - makket in aparte thread foar elke taak dy't derop wurdt útfierd. Kin nuttich wêze foar taken dy't unfoarspelber lange tiid nimme om te foltôgjen.

Der is in goede detaillearre artikel oer TaskSchedulers op it Microsoft-blog.

Foar handige debuggen fan alles relatearre oan Tasks, hat Visual Studio in Tasks-finster. Yn dit finster kinne jo de aktuele tastân fan 'e taak sjen en springe nei de op it stuit útfierende koaderigel.

.NET: Tools foar wurkjen mei multithreading en asynchrony. Diel 1

PLinq en de Parallelle klasse

Neist Taken en alles oer har sein, binne d'r twa mear nijsgjirrige ark yn .NET: PLinq (Linq2Parallel) en de Parallel-klasse. De earste belooft parallelle útfiering fan alle Linq-operaasjes op meardere triedden. It oantal threads kin wurde konfigureare mei de WithDegreeOfParallelism-útwreidingmetoade. Spitigernôch hat PLinq yn syn standertmodus meastentiids net genôch ynformaasje oer de ynterne fan jo gegevensboarne om in signifikante snelheidswinst te leverjen, oan 'e oare kant binne de kosten fan besykjen heul leech: jo moatte gewoan de AsParallel-metoade neame foardat de keatling fan Linq-metoaden en útfiere prestaasjestests. Boppedat is it mooglik om oanfoljende ynformaasje troch te jaan oan PLinq oer de aard fan jo gegevensboarne mei it Partition-meganisme. Jo kinne mear lêze hjir и hjir.

De Parallel statyske klasse jout metoaden foar iterearjen troch in Foreach-kolleksje parallel, it útfieren fan in For-loop, en it útfieren fan meardere ôffurdigen yn parallel Invoke. De útfiering fan 'e aktuele tried sil wurde stoppe oant de berekkeningen binne foltôge. It oantal triedden kin ynsteld wurde troch ParallelOptions troch te jaan as it lêste argumint. Jo kinne ek TaskScheduler en CancellationToken opjaan mei opsjes.

befinings

Doe't ik dit artikel begon te skriuwen op basis fan de materialen fan myn rapport en de ynformaasje dy't ik sammele tidens myn wurk dêrnei, hie ik net ferwachte dat der safolle fan wêze soe. No, as de tekstbewurker dêr't ik dit artikel yn typ my ferwytend fertelt dat side 15 ferdwûn is, sil ik de tuskenresultaten gearfetsje. Oare trúkjes, API's, fisuele ark en falkûlen sille wurde behannele yn it folgjende artikel.

Konklúzjes:

  • Jo moatte de ark kenne foar wurkjen mei threads, asynchrony en parallelisme om de middels fan moderne PC's te brûken.
  • .NET hat in protte ferskillende ark foar dizze doelen
  • Se ferskynden net allegear tagelyk, dus jo kinne faaks legacy fine, lykwols binne d'r manieren om âlde API's te konvertearjen sûnder folle muoite.
  • It wurkjen mei diskusjes yn .NET wurdt fertsjintwurdige troch de Thread- en ThreadPool-klassen
  • De metoaden Thread.Abort, Thread.Interrupt en Win32 API TerminateThread binne gefaarlik en wurde net oanrikkemandearre foar gebrûk. Ynstee dêrfan is it better om it CancellationToken-meganisme te brûken
  • Flow is in weardefolle boarne en it oanbod is beheind. Situaasjes wêr't diskusjes drok binne te wachtsjen op eveneminten moatte foarkommen wurde. Hjirfoar is it handich om de klasse TaskCompletionSource te brûken
  • De machtichste en avansearre .NET-ark foar wurkjen mei parallelisme en asynchrony binne Taken.
  • De c # async / await-operators ymplementearje it konsept fan net-blokkearjende wachtsjen
  • Jo kinne de ferdieling fan Taken oer diskusjes kontrolearje mei TaskScheduler-ôflaat klassen
  • De ValueTask-struktuer kin nuttich wêze by it optimalisearjen fan hot-paden en ûnthâldferkear
  • Visual Studio's Tasks and Threads-finsters jouwe in soad ynformaasje nuttich foar it debuggen fan multi-threaded as asynchrone koade
  • PLinq is in cool ark, mar it hat miskien net genôch ynformaasje oer jo gegevensboarne, mar dit kin wurde reparearre mei it partitioneringsmeganisme
  • Oanhâlde wurde ...

Boarne: www.habr.com

Add a comment