Avaldan Habr'is artikli originaali, mille tÔlge on ettevÔtte .
Vajadus teha midagi asĂŒnkroonselt, oodates tulemusi siin ja praegu, vĂ”i jagada suurt tööd mitme tĂ€itmise ĂŒksuse vahel, eksisteeris juba enne arvutite teket. Arvutite tulekuga muutus see vajadus vĂ€ga tuntavaks. Praegu, 2019. aastal, kirjutades seda artiklit Intel Core'i 8 tuumaga sĂŒlearvutis, mis samal ajal kĂ€itab mitte ĂŒhte sada protsessi, vaid veelgi rohkem niite. KĂ”rval lebab veidi kulunud telefon, mis osteti paar aastat tagasi ja millel on 8 tuumaga protsessor. Teemakohastes ressurssides on hulk artikleid ja videoid, kus nende autorid imetlevad tĂ€navuste lipulaevade nutitelefone, kuhu on paigaldatud 16 tuumaga protsessorid. MS Azure pakub vĂ€hem kui 20 $ / tunni eest virtuaalmasinat, millel on 128 tuumaga protsessor ja 2 TB RAM. Kahjuks on vĂ”imatu saavutada maksimaalset kasu ja taltsutada seda jĂ”udu, kui ei osata hallata niitide koostööd.
Terminoloogia
Protsess (Process) â OS objekt, isoleeritud aadressiruum, sisaldab niite.
Niit (Thread) â operatsioonisĂŒsteemi objekt, vĂ€ikseim tĂ€itmise kogus, protsessi osa, mille jooksul niidid jagavad mĂ€lu ja muid ressursse ĂŒksteisega protsessi raames.
Mitme ĂŒlesande tĂ€itmine â operatsioonisĂŒsteemi omadus, mis vĂ”imaldab samaaegselt tĂ€ita mitut protsessi
Mitme sĂŒdamiku olemasolu â protsessori omadus, mis vĂ”imaldab kasutada mitut sĂŒdamikku andmete töötlemiseks
Mitme protsessori olemasolu â arvuti omadus, mis vĂ”imaldab fĂŒĂŒsiliselt samaaegselt töötada mitme protsessoriga
Mitme niidi olemasolu â protsessi omadus, mis vĂ”imaldab andmete töötlemist jagada mitme niidi vahel.
Paralleelsus â mitme tegevuse fĂŒĂŒsiline samaaegne tĂ€itmine ajaĂŒhikus
AsĂŒnkroonsus â operatsiooni tĂ€itmine ilma ootamata selle töötlemise lĂ”puleviimist, tulemuse töötlemine vĂ”ib toimuda hiljem.
Metafora
Kaugelki kÔik mÀÀratlused ei ole head ja mÔned vajavad tÀiendavat selgitust, seetÔttu lisan formaalselt sisseviidud terminoloogiale hommikusöögi tegemise metafora. Hommikusöögi valmistamine selles metaforas on process.
Hommikust sĂŒĂŒa valmistades (CPU) astun ma kööki (Arvuti). Mul on 2 kĂ€tt (SĂŒdamikud). Köögis on mitmeid seadmeid (IO): ahju, veekeetja, röster, kĂŒlmik. LĂŒlitan gaasi sisse, panen pannile ja valan sinna Ă”li, ootamata, kuni see kuumeneb (asĂŒnkroonselt, Non-Blocking-IO-Wait), tĂ”mban kĂŒlmikust vĂ€lja munad ja löön need kaussi, seejĂ€rel vahustan ĂŒhe kĂ€ega (Thread#1), samas kui teisega (Thread#2) hoian kaussi (Shared Resource). Praegu vĂ”iks ka veekeetjat sisse lĂŒlitada, aga kĂ€si pole piisavalt (Thread Starvation) Selle aja jooksul kuumeneb pann (Tulemuse töötlemine), kuhu valan vahustatud segu. TĂ”mban veekeetja ligi ja lĂŒlitan selle sisse ning vaatangi lihtsalt, kuidas vesi keeb (Blocking-IO-Wait), kuigi oleksin saanud selle aja jooksul nĂ”usid pesta, kus vahustasin omletti.
Valmistasin omletti, kasutades ainult 2 kÀtt, rohkem mul pole, aga samas vahustamise hetkel toimusid korraga 3 operatsiooni: omleti vahustamine, kausi hoidmine, pannikuumuse tÔstmine. CPU on arvuti kÔige kiirem osa, IO on see, mis tihti mahutab, seega on sageli efektiivne lahendus hoida CPU tegevuses, samal ajal kui IO andmete hankimine toimub.
JĂ€tkates metafoori:
- Kui mulle peaks omletti kĂŒpsetades samal ajal riideid vahetama, oleks see multitasking. Oluline nĂŒanss: selles osas on arvutid palju paremad kui inimesed.
- Köök mitme kokaga, nagu restoranis â mitme tuumaga arvuti.
- Palju restorane toidukohtades kaubanduskeskuses â andmekeskus.
Tööriistad .NET.
.NET on voogude haldamisel samuti nagu paljus muus hea. Iga uue versiooniga tutvustatakse jĂ€rjest rohkem uusi tööriistu nende haldamiseks ning uusi abstraheerimise kihte opsĂŒsteemi voogude peale. Abstraheerimise ĂŒlesehitamisel kasutab raami arendaja lĂ€henemist, mis vĂ”imaldab kĂ”rgetasemelise abstraktsiooni kasutamisel laskuda ĂŒhe vĂ”i mitme taseme vĂ”rra madalamale. KĂ”ige sagedamini ei ole selles vajadust, isegi rohkem, see avab vĂ”imaluse endale kahuripauguga jalga tulistada, kuid mĂ”nikord, harvadel juhtudel, vĂ”ib see olla ainus viis probleemi lahendamiseks, mis ei lahene praegusel abstraktsioonitasemel.
Tööriistade all mÔtlen ma nii tarkvaraliideseid (API), mida pakuvad raamistikud ja kolmandate osapoolte paketid, kui ka tÀielikke tarkvaralahendusi, mis lihtsustavad mitme lÔime koodiga seotud probleemide leidmist.
LÔime kÀivitamine
Thread klass, kĂ”ige alus .NET'is lĂ”ime töötamiseks. Konstruktorisse vĂ”etakse ĂŒks kahest delegeeritust:
- ThreadStart â pole parameetreid
- ParametrizedThreadStart â ĂŒhe parameetriga tĂŒĂŒbiga object.
Delegeeritud kĂ€ivitatakse Ă€sja loodud lĂ”imes pĂ€rast Start meetodi kutsumist. Kui konstruktorisse on antud delegeeritud tĂŒĂŒp ParametrizedThreadStart, siis tuleb Start meetodile edastada objekt. See mehhanism on vajalik, et edastada igasugust kohalikku teavet lĂ”ime. Tuleb mĂ€rkida, et lĂ”ime loomine on kulukas operatsioon ja lĂ”im ise on keeruline objekt, kuna vĂ€hemalt 1 MB mĂ€lu eraldatakse virnale ning see nĂ”uab suhtlemist operatsioonisĂŒsteemi API-ga.
new Thread(...).Start(...);
ThreadPool klass esindab puulide kontseptsiooni. .NET-is on lÔimede bassein tehnilise meisterlikkuse teos ja Microsofti arendajad on teinud tohutult palju pingutusi, et see töötaks erinevates stsenaariumides optimaalselt.
Ăhine kontseptsioon:
Alates rakenduse kĂ€ivitamisest loob rakendus taustal mitu reservi lĂ”ime ja pakub neid kasutada. Kui lĂ”ime kasutatakse sageli ja suures koguses, siis laieneb reserv, et rahuldada kutse koodi vajadusi. Kui teatav hetk ei ole vabas reservis vabu lĂ”ime, ootab see kas ĂŒhe lĂ”ime tagasitulekut vĂ”i loob uue. Seega sobib lĂ”imede reserv suurepĂ€raselt lĂŒhikesteks toiminguteks ja on halvem valik teenusteks, mis töötavad rakenduste kogu tööaja jooksul.
Reservist lĂ”ime kasutamiseks on olemas meetod QueueUserWorkItem, mis aktsepteerib WaitCallback tĂŒĂŒpi delegaat, mis vastab parametreeritud lĂ”ime algatamise signatuurile, ja sellele edasi antud parameeter tĂ€idab sama funktsiooni.
ThreadPool.QueueUserWorkItem(...);
VĂ€hem tuntud meetod lĂ”imede reservist RegisterWaitForSingleObject on mĂ”eldud blokeerivate IO-opatsioonide korraldamiseks. See meetod kutsub esile delegaadi, kui selle meetodiga antud WaitHandle on âvabastatudâ (Released).
ThreadPool.RegisterWaitForSingleObject(...)
Kui .NET-is on voogude taimer, erineb see WinForms/WPF taimeritest sellega, et selle kÀsitleja kutsutakse vÀlja basseinist vÔetud voos.
System.Threading.Timer
On ka ĂŒsna eksootiline viis edastada delegaat tĂ€itmiseks basseinist voosse â meetod BeginInvoke.
DelegateInstance.BeginInvoke
Soovin veel vaevu peatuda funktsioonil, millele paljuski viitavad eespool nimetatud meetodid â CreateThread Kernel32.dll Win32 API-s. On olemas vĂ”imalus, tĂ€nu extern meetodite mehhanismile, kutsuda seda funktsiooni. Olen nĂ€inud sellist kutsumist ainult kord hirmsas vanas koodinĂ€ites, ja autori motivatsioon, kes just nii tegutses, jÀÀb mulle ikka veel mĂ”istatuseks.
Kernel32.dll CreateThread
Voogude vaatamine ja tÔrkeotsing
Teie loodud kĂ”ik kolmandate osapoolte komponentide ja .NET programmivekte vĂ”ib Visual Studio Threadsi aknas vaadata. See aken kuvab teavet ainult siis, kui rakendus on silumisreĆŸiimis ja katkestatud. Siin saab mugavalt vaadata iga tee steki nimesid ja prioriteete, samuti suunata silumist konkreetsele teele. Threadi klassi Priority omadus vĂ”imaldab mÀÀrata tee prioriteedi, mida OC ja CLR mĂ”istavad soovitusena, kui jagatakse protsessoritööaega tee vahel.

Task Parallel Library
Task Parallel Library (TPL) ilmus vĂ€lja .NET 4.0. TĂ€na on see standardne ja peamine tööriist asĂŒnkroonsuse rakendamiseks. Iga kood, mis kasutab vanemaid lĂ€henemisi, loetakse pĂ€randiks. TPL pĂ”hiĂŒhik on Task klass, mis kuulub sĂŒsteemi System.Threading.Tasks. Task esindab abstraktsiooni lĂ”ngast. Uue C# versiooniga saime elegantse viisi Task`idega töötamiseks â async/await operaatorid. Need kontseptsioonid vĂ”imaldavad kirjutada asĂŒnkroonsed koodid justkui need oleksid lihtsad ja sĂŒnkroossed, andes vĂ”imaluse isegi neile, kes ei mĂ”ista sĂŒgavalt lĂ”ngade toimimist, luua rakendusi, mis ei jÀÀ kinni pikade operatsioonide tĂ€itmisel. Async/await kasutamine on teema, mis vÀÀrib vĂ€hemalt ĂŒhte, kui mitte rohkem artiklit, kuid ma ĂŒritan lĂŒhidalt kokku vĂ”tta selle sisemise tĂ€henduse:
- async on meetodi modifikaator, mis tagastab Task vÔi void
- ja await on Task`i mitteblokeeriv ootamise operaator.
Kordame: await operaator, ĂŒldiselt (on erandeid), laseb praegusel tĂ€itmisvoolul edasi minna, ja kui Task lĂ”petab oma tĂ€itmise, siis kui kontekst (tĂ”eliselt on Ă”igem öelda kontekst, aga sellest hiljem) on vaba, jĂ€tkab meetodi tĂ€itmisega edasi. .NET-is on see mehhanism rakendatud sarnaselt yield return'iga, kui kirjutatud meetod muudetakse tervikuks klassiks, mis on olekute masin ja saab tĂ€ita eraldi osades, sĂ”ltuvalt nendest olekutest. Need, keda huvitab, vĂ”ivad kirjutada mis tahes lihtsa koodi asynŃ/await kasutades, kompileerida selle ja vaadata kogumit JetBrains dotPeek'iga, aktiveeritud Compiler Generated Code'iga.
Vaatame Task'i kÀivitamise ja kasutamise vÔimalusi. Allolevas koodis loome uue task'i, mis ei tee midagi kasulikku (Thread.Sleep(10000)), kuid reaalses elus peaks see olema mingi keeruline protsessorit mÔjutav töö.
kasutades TCO = System.Threading.Tasks.TaskCreationOptions;
public static async void VoidAsyncMethod() {
var cancellationSource = new CancellationTokenSource();
await Task.Factory.StartNew(
// Tegevus kodeeritakse teises kontekstis
() => Thread.Sleep(10000),
cancellationSource.Token,
TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness,
scheduler
);
// Kood pÀrast awaiti tÀidetakse kinni haaratud kontekstis
}
Task luuakse rea valikutega:
- LongRunning â vihje, et ĂŒlesanne ei pruugi kiiresti tĂ€idetav olla, seega oleks mĂ”istlik kaaluda, et mitte kasutada threadâit puul, vaid luua eraldi selle Taskâi jaoks, et mitte kahjustada teisi.
- AttachedToParent â Taskâid vĂ”ivad moodustada hierarhia. Kui see valik on tehtud, vĂ”ib Task olla seisundis, kus see on ise tĂ€idetud ja ootab alamĂŒlesannete tĂ€itmist.
- PreferFairness â tĂ€hendab, et oleks hea tĂ€ita varem saadetud Taskâid enne hiljem saadetud ĂŒlesandeid. Kuid see on vaid soovitus ja tulemuse tagamine ei ole garanteeritud.
Teiseks parameetriks, mis meetodile edastatakse, on CancellationToken. Et töötlus toimuks Ôigesti pÀrast tegevuse kÀivitamist, peab tÀidetav kood sisaldama CancellationTokeni oleku kontrollimise kontrolle. Kui kontrollimist pole, siis saab CancellationTokenSource'ilt kutsutud Cancel meetod peatada Task'i ainult enne selle kÀivitamist.
Viimaseks parameetriks on edastatud TaskScheduler tĂŒĂŒpi objekt scheduler. See klass ja selle alamklassid on mĂ”eldud Task'ide jaotamisstrateegiate haldamiseks, vaikimisi tĂ€idetakse Task juhuslikul niidi jooksul pidevas niidi plokis.
Loodud Taskile on rakendatud await operaator, mis tÀhendab, et pÀrast seda kirjutatud kood, kui selline on, tÀidetakse samas kontekstis (tihti tÀhendab see, et samas niidis), nagu oli kood enne await'i.
Meetod on mĂ€rgistatud kui async void, mis tĂ€hendab, et see lubab kasutada await operaatorit, kuid kutsuv kood ei saa oodata tĂ€itmist. Kui see on vajalik, peaks meetod tagastama Taskâi. Async void meetodeid esineb ĂŒsna sageli: tavaliselt on need sĂŒndmuste töötlejate vĂ”i teiste meetodite puhul, mis toimivad pĂ”himĂ”ttel 'tee ja unusta' (fire and forget). Kui on vajalik vĂ”imalus oodata tĂ€itmise lĂ”ppu ja tagastada tulemus, tuleb kasutada Taskâi.
Taskâilt, mille meetod StartNew tagastas, saab nagu igalt teiselt, kutsuda vĂ€lja ConfigureAwait meetodi false parameetriga, sel juhul jĂ€tkub tĂ€itmine pĂ€rast awaitâi mitte haaratud kontekstis, vaid suvalises. Seda tuleb alati teha, kui awaitâi jĂ€rgneva koodi tĂ€itmise kontekst ei ole oluline. See on ka MS-i soovitus koodi kirjutamisel, mis pakendatakse teegina.
Vaatame veel natuke, kuidas saab oodata Taskâi tĂ€itmise lĂ”ppu. Allpool on koodinĂ€ide koos kommentaaridega, kus ootamine on tehtud tinglikult hĂ€sti ja kus tinglikult halvasti.
public static async void AnotherMethod() {
int result = await AsyncMethod(); // hea
result = AsyncMethod().Result; // halb
AsyncMethod().Wait(); // halb
IEnumerable tasks = new Task[] {
AsyncMethod(), OtherAsyncMethod()
};
await Task.WhenAll(tasks); // hea
await Task.WhenAny(tasks); // hea
Task.WaitAll(tasks.ToArray()); // halb
}
Esimeses nĂ€ites ootame, kuni ĂŒlesanne on lĂ”pule viidud, ilma et peataksime kutsuva tĂ€itev, tulemuse töötlemisse naaseme alles siis, kui see on juba saadaval, kuni selle ajani kutsev tĂ€itev jÀÀb endale.
Teises variandis peatame kutsuva tĂ€itevni, kuni meetodi tulemus on arvutatud. See on halb, mitte ainult seetĂ”ttu, et oleme kinni hoidnud niivĂ”rd vÀÀrtuslikku ressursi, nagu tĂ€itev, vaid ka seetĂ”ttu, et kui meetodi taastumise koodis on await ja sĂŒnkroneerimise kontekst eeldab, et pĂ€rast await naaseme originaalsesse tĂ€ituvasse, saame deadlock'i: kutsuv tĂ€itev ootab, kuni asĂŒnkroonne meetod on arvutatud, asĂŒnkroonne meetod ĂŒritab meeleheitlikult jĂ€tkata oma tĂ€itmist kutsuvas tĂ€ites.
Ăks sellise lĂ€henemise puudusi on vea töötlemise keerukus. Asynchronous koodiga töötades on async/await vead vĂ€ga lihtsalt töödeldavad â need kĂ€ituvad nagu sĂŒnkroonne kood. Kui aga rakendame sĂŒnkroonset oote protsessi, siis originaalne erand mĂ€hitakse AggregateException'i, mistĂ”ttu selle töötlemiseks tuleb uurida InnerException tĂŒĂŒpi ja kirjutada omamoodi if-ahel ĂŒhe catch ploki sees vĂ”i kasutada catch when konstruktsiooni, selle asemel et kasutada C# maailmas tuttavaid catch plokkide ahelat.
Kolmas ja viimane nÀide on samuti halva kvaliteediga sama pÔhjuse tÔttu ning sisaldab kÔiki neid samu probleeme.
Meetodid WhenAny ja WhenAll on ÀÀrmiselt mugavad Taskâide grupi oote tegemiseks, nad mĂ€hivad Taskâide grupi ĂŒhte, mis aktiveerub kas esimesel Taskâil rikke korral vĂ”i siis, kui kĂ”ik on oma tĂ€itmise lĂ”petanud.
Protsesside peatamine
Erinevatel pĂ”hjustel vĂ”ib tekkida vajadus peatada protsess pĂ€rast selle kĂ€ivitamist. Selleks on olemas mitmeid meetodeid. Thread klassil on kaks meetodit sobivate nimedega â need on Abort ja Interrupt. Esimene on ÀÀrmiselt ebasoovitav, kuna selle kutsumise tĂ”ttu viskatakse erand juhuslikul hetkel, olenemata millise kĂ€su töötlemisest. ThreadAbortedException. Te siiski ei oota, et selline erand tekib mingi tĂ€isarvu muutuja suurendamise ajal, eks? Kuid selle meetodi kasutamisel on see tĂ€iesti reaalne olukord. Kui on vajadus keelata CLR-i genereerida selline erand teatud koodilĂ”igus, saab seda ĂŒmbritseda kutsetega. Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Need kutsed ĂŒmbritsevad kogu koodi, mis on kirjutatud finally plokki. SellepĂ€rast vĂ”ib raamistiku koodist leida tĂŒhjad try plokid, kuid mitte tĂŒhjad finally plokid. Microsoft ei soovita seda meetodit kasutada nii vĂ€ga, et nad ei ole seda sisse lĂŒkanud .net core'i.
Meetod Interrupt töötab ennustatavamalt. See vÔib katkestada niidi erandi ThreadInterruptedException ainult siis, kui niit on ooteseisundis. Sellesse olekusse siseneb see, kui see jÀÀb ootele WaitHandle'i, lukustuse (lock) vÔi pÀrast Thread.Sleep'i kutset.
MĂ”lemad ĂŒlaltoodud variandid on halvad oma ennustamatuse tĂ”ttu. Lahenduseks on struktuuri kasutamine. CancellationToken ja klassi CancellationTokenSource. KĂ€ibekĂ€ik on jĂ€rgmine: luuakse CancellationTokenSource klassi eksemplar, ja ainult selle omanik saab lĂ”petada operatsiooni, kutsudes vĂ€lja meetodi Cancel. Operatsiooni edastatakse ainult CancellationToken. CancellationToken'i omanikud ei saa operatsiooni ise lĂ”petada, kuid nad saavad kontrollida, kas operatsioon on lĂ”petatud. Selle jaoks on boolean omadus IsCancellationRequested ja meetod ThrowIfCancelRequested. Viimane genereerib erandi TaskCancelledException kui kutsutakse vĂ€lja Cancel meetod sellel CancellationTokenSource'i eksemplaril. Just seda meetodit ma soovitan kasutada. See annab parema kontrolli ĂŒle, millal operatsiooni erand vĂ”ib katkestada.
KĂ”ige ÀÀrmuslikum viis lĂ”imede peatamiseks on Win32 API funktsiooni TerminateThread kutsumine. CLR-i kĂ€itumine pĂ€rast selle funktsiooni vĂ€ljakutsumist vĂ”ib olla ettearvamatu. MSDN-s on selle funktsiooni kohta öeldud jĂ€rgmist: âTerminateThread on ohtlik funktsioon, mida tuleks kasutada ainult kĂ”ige ÀÀrmuslikumates olukordades.â
Legacy-API muutmine ĂŒlesande-pĂ”hiseks meetodi FromAsync abil
Kui teil on olnud vĂ”imalus töötada projekti kallal, mis algas juba pĂ€rast seda, kui ĂŒlesanded olid sisse viidud ja enamiku arendajate seas vaikset kohut tekitasid, siis ei pea te tegelema paljude vanade API-dega, olgu need siis kolmandate osapoolte vĂ”i teie meeskonna varem loodud. Ănneks hoolitses .NET Frameworki arendustiim meie eest, ehkki vĂ”ib-olla oli nende eesmĂ€rgiks enda eest hoolitsemine. Igatahes on .NET-is terve hulk tööriistu, mis vĂ”imaldavad valutult ĂŒmber kodeerida vana asĂŒnkroonse programmeerimise lĂ€henemise uueks. Ăks neist on TaskFactory meetod FromAsync. Allpool toodud koodi nĂ€itel olen ma vanad asĂŒnkroonsed meetodid WebRequest klassist ĂŒmber pannud Task'iks kasutades seda meetodit.
object state = null;
WebRequest wr = WebRequest.CreateHttp("http://github.com");
await Task.Factory.FromAsync(
wr.BeginGetResponse,
wr.EndGetResponse
);
See on vaid nĂ€ide ja sellise asja tegemine sisseehitatud tĂŒĂŒpidega ei ole tĂ”enĂ€oliselt vajalik, kuid iga vana projekt on lihtsalt tĂ€is meetodeid BeginDoSomething, mis tagastavad IAsyncResult ja meetodeid EndDoSomething, mis neid vastu vĂ”tavad.
Legacy-API muutmine ĂŒlesannete pĂ”hiseks Task Based lĂ€henemiseks kasutades klassi TaskCompletionSource
Veel ĂŒks oluline kaalutav tööriist on klass TaskCompletionSource. Funktsioonide, otstarbe ja tööpĂ”himĂ”tte poolest vĂ”ib see meenutada ThreadPooli RegisterWaitForSingleObject meetodit, millest ma eespool rÀÀkisin. Selle klassi abil on lihtne ja mugav vanu asĂŒnkroonseid API-sid Taskâi-deks ĂŒmber muuta.
Ătlete, et ma olen juba rÀÀkinud TaskFactory FromAsync meetodist, mis on nende eesmĂ€rkide saavutamiseks mĂ”eldud. Peame meenutama kogu asĂŒnkroonsete mudelite arengulugu .net-is, mida Microsoft on viimase 15 aasta jooksul pakkunud: enne Task-Based Asynchronous Pattern (TAP) eksisteeris Asynchronous Programming Pattern (APP), mis keskendus meetoditele. BeginDoSomething, mis tagastab IAsyncResult ja meetoditega EndDoSomething, mis seda aktsepteerib ja noorte pĂ€randi jaoks sobib FromAsync meetod ideaalselt, kuid aja jooksul asendas selle Event Based Asynchronous Pattern (EAP), mis eeldas, et asĂŒnkroonse tegevuse lĂ”petamisel kutsutakse vĂ€lja sĂŒndmus.
TaskCompletionSource sobib suurepĂ€raselt legacy-API-dele, mis on ĂŒles ehitatud sĂŒndmuste mudelile, pakkumiseks. Selle tööpĂ”himĂ”te on jĂ€rgmine: selle klassi objekt omab avalikku omadust, mille tĂŒĂŒp on Task, mille olekut saab hallata lĂ€bi meetodite SetResult, SetException jne. TaskCompletionSource klassis. Seal, kus on rakendatud await operaator sellele Taskâile, tĂ€idetakse see vĂ”i tĂ”mmatakse erandiga sĂ”ltuvalt TaskCompletionSource meetodist, mida on rakendatud. Kui see pole ikka veel selge, siis vaatame seda koodinĂ€idet, kus mingi vana API EAP ajast pakitakse Task'iks TaskCompletionSource abil: sĂŒndmuse korralduse puhul viiakse Task Completed olekusse ja meetod, mis rakendas sellele Taskâile await operaatori, jĂ€tkab oma tĂ€itmist, saades objekti. tulemus.
public static Task DoAsync(this SomeApiInstance someApiObj) {
var completionSource = new TaskCompletionSource();
someApiObj.Done +=
result => completionSource.SetResult(result);
someApiObj.Do();
return completionSource.Task;
}
TaskCompletionSource nÀpunÀited ja trikid
Vana API mĂ€his ei ole kaugeltki ainus, mida saab saavutada TaskCompletionSource'i abil. Selle klasi kasutamine avab huvitava vĂ”imaluse erinevate API-de projekteerimiseks, mis pĂ”hinevad ĂŒlesannetel, mis ei kasutata thread'e. Ja nagu me teame, on thread kallis ressurss ning nende arv on piiratud (peamiselt RAM-i mahuga). Seda piirangut on lihtne saavutada, kui arendada nĂ€iteks koormatud veebirakendust keerulise Ă€riloogikaga. Vaatame neid vĂ”imalusi, millest ma rÀÀgin niisuguse triki nagu Long-Polling rakendamisel.
LĂŒhidalt öeldes seisneb trikk selles: peate saama API-lt teavet teatud sĂŒndmuste kohta, mis toimuvad tema poolel, kuid mingil pĂ”hjusel ei saa API sĂŒndmust teatada, vaid saab ainult tagasi anda oleku. NĂ€iteks kĂ”ik API-d, mis on loodud HTTP peal enne WebSocketi aega vĂ”i kui mingil pĂ”hjusel ei saa seda tehnoloogiat kasutada. Klient vĂ”ib kĂŒsida HTTP serverilt. HTTP server ei saa ise suhtlemist kliendiga algatada. Lihtne lahendus on serveri kĂŒsitlemine ajastuse pĂ”hjal, kuid see tekitab serverile tĂ€iendava koormuse ja lisaviivituse keskmiselt TimerInterval / 2. Selle vĂ€ltimiseks leiutati trikk, mida nimetatakse pika kĂŒsitluse (Long Polling) meetodiks, mis eeldab vastuse viivitust serverist kuni Timeout'i lĂ”ppemiseni vĂ”i sĂŒndmuse toimumiseni. Kui sĂŒndmus toimub, siis see töödeldakse, kui mitte, siis saadetakse pĂ€ring uuesti.
while(!eventOccures && !timeoutExceeded) {
CheckTimout();
CheckEvent();
Thread.Sleep(1);
}
Kuid selline lahendus osutub katastroofiliseks, kui oodatavate sĂŒndmuste arv kasvab, kuna iga selline klient, kes ootab sĂŒndmust, hĂ”ivab terve voolu. Lisaks saame me sĂŒndmuse toimumise puhul 1 ms lisaviivituse, mis enamasti ei ole mĂ€rkimisvÀÀrne, kuid miks teha tarkvara halvemaks, kui see vĂ”iks olla? Kui aga eemaldada Thread.Sleep(1), koormame me kasutu tsĂŒkli tĂ”ttu ĂŒhe protsessorituuma 100% ulatuses, pöörledes asjatult. TaskCompletionSource'i abil saame selle koodi hĂ”lpsasti ĂŒmber kirjutada ja lahendada kĂ”ik eelpool mainitud probleemid:
class LongPollingApi {
private Dictionary<int, TaskCompletionSource> tasks;
public async Task AcceptMessageAsync(int userId, int duration) {
var cs = new TaskCompletionSource();
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 ei ole tootmisvalmis, vaid lihtsalt demo. Reaalsetes olukordades on vajalik veel vÀhemalt töödelda olukorda, kui sÔnum saabus hetkel, mil keegi seda ei oodanud: sellisel juhul peaks meetod AsseptMessageAsync tagastama juba lÔpetatud Taski. Kui see olukord on tÔesti kÔige sagedasem, vÔib kaaluda ka ValueTaski kasutamist.
SÔnumi pÀringu saamisel loome ja paneme pingelisse TaskCompletionSource'i ning seejÀrel ootame, mis juhtub varem: kas mÀÀratud ajavahemik aegub vÔi saabub sÔnum.
ValueTask: miks ja kuidas
Async/await operaatorid, nagu ka yield return operaator, loovad meetodist olekumasina. See tĂ€hendab uue objekti loomist, mis enamasti pole oluline, kuid harvadel juhtudel vĂ”ib see pĂ”hjustada probleeme. Selliseks juhtumiks vĂ”ib olla meetod, mida tĂ”epoolest sageli kutsutakse â kĂ”ne on kĂŒmnetest ja sadadest tuhandetest kutsumistest sekundis. Kui selline meetod on kirjutatud nii, et enamikul juhtudel tagastab ta tulemuse, mööda minnes kĂ”igist await meetoditest, siis .NET pakub tööriista selle optimeerimiseks â ValueTask struktuur. Arusaamaks, vaadakem nĂ€iteks selle kasutamist: on vahemĂ€lu, mida me kĂŒlastame vĂ€ga tihti. MĂ”ned vÀÀrtused on seal ja siis me lihtsalt tagastame need, kui neid pole, siis lĂ€heme aeglase IO poole nende jĂ€rele. Viimast tahame teha asĂŒnkroonselt, seega muutub kogu meetod asĂŒnkroonseks. Seega on ilmne variant meetodi kirjutamiseks jĂ€rgmine:
public async Task GetById(int id) {
if (cache.TryGetValue(id, out string val))
return val;
return await RequestById(id);
}
Soovides veidi optimeerida ja vÀiksest hirmust selle suhtes, mida Roslyn kompileerides genereerib, saab seda nÀidet kirjutada jÀrgmiselt:
public Task GetById(int id) {
if (cache.TryGetValue(id, out string val))
return Task.FromResult(val);
return RequestById(id);
}
Kuid kÔige optimaalsem lahendus on selles olukorras hot-pathi optimeerimine, nimelt vÀÀrtuse saamine sÔnastikust ilma tÀiendavate jaotuste ja GC koormuseta, samas kui harvadel juhtudel, kui peab IO-st andmete jÀrele minema, jÀÀb kÔik enam-vÀhem samaks.
public ValueTask GetById(int id) {
if (cache.TryGetValue(id, out string val))
return new ValueTask(val);
return new ValueTask(RequestById(id));
}
Vaatame seda koodifragmenti lĂ€hemalt: kui vÀÀrtus on vahemikus olemas, loome struktuuri, vastasel juhul ĂŒmbritsetakse tegelik ĂŒlesanne oluliseks. Kutsuv kood ei hooli, millist teed see kood jĂ€rgnes: ValueTask kĂ€itub C# sĂŒntaksis samamoodi nagu tavaline Task selles osas.
TaskScheduler'id: Task'ide kÀivitamisstrateegiate haldamine
JĂ€rgmine API, mida sooviksime arutada, on klass TaskScheduler ja selle tuletised. Olen juba varem maininud, et TPL-is on vĂ”imalik hallata ĂŒlesannete jaotamise strateegiaid voogude vahel. Sellised strateegiad mÀÀratletakse TaskScheduleri klassi jĂ€rglastes. Praktikas leidub praktiliselt igasuguseid strateegiaid, mis vĂ”ivad olla vajalikud, raamatukogust. ParallelExtensionsExtras, mis on vĂ€lja töötatud Microsofti poolt, kuid ei ole osa .NET-ist, vaid tarnitakse Nuget paketi kujul. Vaatame kiirelt mĂ”nda neist:
- CurrentThreadTaskScheduler â tĂ€idab ĂŒlesandeid ŃĐ”ĐșŃŃĐ”ĐŒ voos
- LimitedConcurrencyLevelTaskScheduler â piirab samaaegselt tĂ€idetavate ĂŒlesannete arvu parameetriga N, mille ta konstruktoris vastu vĂ”tab.
- OrderedTaskScheduler â mÀÀratletakse kui LimitedConcurrencyLevelTaskScheduler(1), seega ĂŒlesanded tĂ€idetakse jĂ€rjestikku.
- WorkStealingTaskScheduler â rakendab ĂŒlesannete jaotamise lĂ€henemine. Tegelikult 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 ĂŒhe osa programmis vĂ”ib pĂ”hjustada kĂ”rvalmĂ”jusid teises. Veelgi enam, selliste defektide pĂ”hjuste mĂ”istmine on ÀÀrmiselt keeruline. Seega vĂ”ib olla vajadus kasutada eraldi WorkStealingTaskScheduler'eid nendes programmi osades, kus ThreadPooli kasutamine vĂ”ib olla agressiivne ja ettearvamatu.
- QueuedTaskScheduler â vĂ”imaldab tĂ€ita ĂŒlesandeid jĂ€rjekorra reeglitega koos prioriteetidega
- ThreadPerTaskScheduler â loob iga Task jaoks eraldi teema, millega seda tĂ€idetakse. VĂ”ib olla kasulik ettearvamatult kaua kestvates ĂŒlesannetes.
On olemas hea ja ĂŒksikasjalik TaskScheduler'ite kohta Microsofti blogis.
KĂ”ikide Task'idega seotud mugavaks tĂ”rkeotsimiseks Visual Studio's on Tasks aken. Selles aknas saab nĂ€ha ĂŒlesande praegust olekut ja minna ĂŒle hetkel tĂ€idetavale koodirea.

PLinq ja Parallel klass
Lisaks Task'idele ja kĂ”igile, mis nendega seondub, on .NET-is veel kaks huvitavat tööriista: PLinq (Linq2Parallel) ja Parallel klass. Esimene lubab kĂ”iki Linq operatsioone tĂ€ita paralleelselt mitmes lĂ”imes. LĂ”imede arvu saab konfigureerida laiendusena Meetodiga WithDegreeOfParallelism. Kahjuks ei piisa PLinq-lt vaikereĆŸiimis sageli andmeallika sisust oluline kiirusetĂ”us, kuid katse hind on vĂ€ga madal: piisab, kui kutsuda meetodit AsParallel enne Linq meetodite kettast ja teha jĂ”udluse teste. Veelgi enam, PLinq lubab edastada tĂ€iendavat teavet teie andmeallika iseloomu kohta Partitions mehhanismi abil. TĂ€iendavat teavet saab lugeda. ja .
Statyline Parallel klass pakub meetodeid kollektsiooni Foreach paralleelseks lĂ€bimiseks, For tsĂŒkli tĂ€itmiseks ja mitme delegaadi paralleelseks nĂ”udmiseks. Kogumise praegune voog peatatakse kuni arvutuste lĂ”petamiseni. Voogude arvu saab konfigureerida, edastades ParallelOptions viimase argumendina. Valikute kaudu saab ka mÀÀrata TaskScheduleri ja CancellationTokeni.
JĂ€reldused
Kui ma hakkasin kirjutama seda artiklit oma ettekande materjalidel ja teabel, mida olen kogunud pĂ€rast seda, ei oodanud ma, et sellest nii palju tuleb. NĂŒĂŒd, kui tekstiredaktor, kus ma seda artiklit koostan, ĂŒtleb mulle sĂŒĂŒdistavalt, et olen jĂ”udnud 15. lehe juurde, teen ma vahekokkuvĂ”tte. Teised nĂ”ksud, API, visuaalsed tööriistad ja takistused arutatakse jĂ€rgmises artiklis.
JĂ€reldused:
- Oluline on tunda tööriistu voogude, asĂŒnkroonsuse ja paralleelsuse kasutamiseks, et Ă€ra kasutada tĂ€napĂ€evaste arvutite ressursse.
- .NET-is on palju erinevaid tööriistu nende eesmÀrkide saavutamiseks.
- Kaugel kÔik need ei ilmunud korraga, seega vÔib sageli kohata legacy-lahendusi, kuid on olemas viise vanade API-de transformeerimiseks ilma eriliste pingutusteta.
- Tööde haldamine .NET-is on esitatud klasside Thread ja ThreadPool kaudu
- Meetodid Thread.Abort, Thread.Interrupt ja Win32 API funktsioon TerminateThread on ohtlikud ja nende kasutamist ei soovitata. Nende asemel on soovitatav kasutada CancellationToken'ide mehhanismi.
- KĂ€thread on vÀÀrtuslik ressurss, nende arv on piiratud. Tuleb vĂ€ltida olukordi, kus thread'id ootavad sĂŒndmuste toimumist. Selleks on mugav kasutada klassi TaskCompletionSource.
- KĂ”ige vĂ”imsamad ja arenenumad tööriistad .NET-is, millega tegeleda paralleelsuse ja asĂŒnkroonsete ĂŒlesannetega, on Task'id.
- C# async/await operaatorid rakendavad mitteblokeeriva ootamise kontseptsiooni.
- Task'ide jaotust thread'ide vahel saab hallata tĂŒĂŒtas TaskScheduler'ist derivaatklassi abil.
- ValueTask struktuur vÔib olla kasulik hot-paths ja mÀluliiklus optimeerimisel.
- Visual Studio Tasks ja Threads aknad pakuvad palju kasulikku teavet, et siluda mitme lĂ”ime vĂ”i asĂŒnkroonse koodi.
- PLinq on lahe tööriist, kuid tal ei pruugi olla piisavalt teavet teie andmeallika kohta, kĂŒll aga saab seda parandada jaotamise mehhanismi abil.
- JĂ€tkubâŠ
Allikas: habr.com
