Avaldan artikli originaali Habris, mille tÔlge on ettevÔtte .
Vajadus teha midagi asĂŒnkroonselt, mitte ootama tulemusi kohe ja praegu, vĂ”i jagada suurt tööd mitme teostava ĂŒksuse vahel, oli olemas juba enne arvutite tulekut. Nende ilmumisega muutus see vajadus vĂ€ga tuntavaks. NĂŒĂŒd, 2019. aastal, kui ma seda artiklit kirjutades kasutan 8-ydre Intel Core protsessoriga sĂŒlearvutit, töötab paralleelselt mitte ĂŒks, vaid terve sadu protsesse, ja veel rohkem kulusid. KĂ”rval on mul veidi kulunud, paar aastat tagasi ostetud telefon, millel on 8-ydre protsessor. Teemakohastes allikates on palju artikleid ja videoid, kus nende autorid imetlevad selle aasta lipulaevade nutitelefone, kuhu paigutatakse 16-ydre protsessorid. MS Azure pakub vĂ€hem kui 20 $/tunnis virtuaalmasinat, millel on 128-ydre protsessor ja 2 TB RAM. Kahjuks on vĂ”imatu maksimaalselt Ă€ra kasutada ja valitseda seda jĂ”udu, kui ei osata juhtida voogude vahelist suhtlemist.
Terminoloogia
Protsess (Process) â OS objekt, isoleeritud aadressiruumi, sisaldab vooge.
Voog (Thread) â OS objekt, vĂ€ikseim tĂ€itmise ĂŒhik, protsessi osa, vood jagavad mĂ€lu ja muid ressursse omavahel protsessi piires.
Ăheaegsus â OS omadus, vĂ”imalus tĂ€ita samaaegselt mitu protsessi
Mitme tuuma kasutamine â protsessori omadus, vĂ”imalus kasutada andmete töötlemiseks mitut tuuma
Mitme protsessori kasutamine â arvuti omadus, vĂ”imalus töötada samaaegselt mitme fĂŒĂŒsilise protsessoriga
Mitmekeelsus â protsessi omadus, vĂ”imalus jaotada andmete töötlemine mitme voolu vahel.
Paralleelsus â mitme tegevuse fĂŒĂŒsiline samaaegne teostamine ĂŒhes ajas
AsĂŒnkroonsus â operatsiooni teostamine ilma selle töötlemise lĂ”petamise ootamata, tulemuse töötlemine vĂ”ib toimuda hiljem.
Metafoor
KÔik mÀÀratlused ei ole head ja mÔned vajavad tÀiendavat selgitust, seetÔttu lisan formaalselt antud terminoloogiale hommikusöögi valmistamise metafoori. Hommikusöögi valmistamine selles metafooris on protsess.
Hommikusööki valmistades (CPU) tulen kööki (Arvuti). Mul on 2 kĂ€tt (Cores). Köögis on mitmeid seadmeid (IO): ahi, veekeetja, röster, kĂŒlmkapp. LĂŒlitan sisse gaasi, panen sellele panni ja valan sinna Ă”li, ootamata, kuni see kuumeneb (asĂŒnkroonne, Non-Blocking-IO-Wait), vĂ”tan kĂŒlmkapist munad ja löön need kaussi, seejĂ€rel vahustan ĂŒhes kĂ€es (Thread#1), samal ajal kui teise (Thread#2) hoian kaussi (Jagatud Ressurss). Praegu vĂ”iks ka veekeetjat sisse lĂŒlitada, aga kĂ€si ei jĂ€tku (Thread Starvation) Selle aja jooksul kuumeneb pann (Tulemuste töötlemine), kuhu valan vahustatud segu. Ulatan veekeetjani ja lĂŒlitan selle sisse ja lihtsalt vaatan, kuidas vesi keema lĂ€heb (Blocking-IO-Wait), kuigi vĂ”iksin antud ajal nĂ”ud pesta, kus ma omletti vahustasin.
Valmistasin omleti kasutades vaid 2 kÀtt, rohkem mul ei ole, kuid samas omleti vahustamise ajal toimus korraga 3 tegevust: omleti vahustamine, kausi hoidmine, pann kuumutamine. CPU on arvuti kÔige kiiremat osa, IO on see, mis tÔrkeb sagedamini, seega on sageli efektiivne lahendus tegeleda millegagi CPU-ga, samal ajal kui andmeid saadakse IO-lt.
JĂ€tkates metafoori:
- Kui omleti valmistamise ajal prooviksin ka riideid vahetada, oleks see mitmekesiste ĂŒlesannete tĂ€itmise nĂ€ide. Oluline nĂŒanss: arvutites on selle osas palju parem kui inimestel.
- Köök mitmete kokkadega, nĂ€iteks restoranis â mitme tuumaline arvuti.
- Palju restorane toidukohtades kaubanduskeskuses â andmekeskus
Tööriistad .NET
.NET on multitegumtöötluse puhul ja paljus muus head. Iga uue versiooniga tutvustatakse aina rohkem uusi tööriistu nendega töötamiseks, uusi abstraktsiooni kihte operatsioonisĂŒsteemi (OS) voogude kohal. Abstraktsioonide loomisel kasutavad raamistikud lĂ€htekohti, mis jĂ€tavad vĂ”imaluse kĂ”rgetasemelise abstraktsiooni kasutamise ajal langetada madalamale tasemele vĂ”i madalamatele tasanditele. Enamasti ei ole see vajalik, tĂ”eliselt see avab vĂ”imaluse ennast juhuslikult vigastada, kuid mĂ”nikord, harvadel juhtudel, vĂ”ib see osutuda ainus viis probleemi lahendamiseks, mis ei lahene praegusel abstraktsioonitasemel.
Tööriistade all mÔtlen ma nii tarkvaraliideseid (API) raamistikult ja kolmandatelt osapooltelt kui ka terviklikke tarkvaralahendusi, mis lihtsustavad mitme lÔimekoodiga seotud probleemide leidmist.
Töö kÀivitamine
Thread klass, kĂ”ige pĂ”hilisem .NET-is lĂ”imede töötlemiseks. Konstruktor vĂ”tab vastu ĂŒhe kahest delegaadist:
- ThreadStart â Ilma parameetriteta
- ParametrizedThreadStart â ĂŒhe object tĂŒĂŒpi parameetriga.
Delegaati tĂ€idetakse uues loodud lĂ€htepunktis pĂ€rast Start meetodi kutsumist; kui konstruktorisse on edastatud ParametrizedThreadStart tĂŒĂŒpi delegaat, tuleb Start meetodile edastada objekt. See mehhanism on vajalik kohaliku info edastamiseks lĂ”ime. Tuleb mĂ€rkida, et lĂ”ime loomine on kulukas operatsioon ning lĂ”im on raske objekt, kuna vĂ€hemalt 1MB mĂ€lu eraldamine toimub virna jaoks ning see nĂ”uab operatsioonisĂŒsteemi API-ga suhtlemist.
new Thread(...).Start(...);
ThreadPool klass esindab kontseptsiooni, mis on seotud puu kasutamisega. .NETis on lÔimede puu inseneri kunst ja Microsofti arendajad on teinud palju pingutusi selle tagamiseks, et see töötaks optimaalselt erinevates stsenaariumides.
Ăldine kontseptsioon:
Alates rakenduse kĂ€ivitamisest loob ta taustal mitmeid reservlĂ”ime ja vĂ”imaldab kasutada neid. Kui lĂ”ime kasutatakse sageli ja suurtes kogustes, siis puu laieneb, et rahuldada kutsuva koodi vajadusi. Kui puus ei ole vaba lĂ”ime, ootab see kas mĂ”ne tagasi tuleku vĂ”i loob uue. SeetĂ”ttu sobib lĂ”imede puu suurepĂ€raselt lĂŒhiajaliste toimingute jaoks, kuid ei sobi teenusteks, mis töötavad kogu rakenduse aja.
LĂ”ime kasutamiseks puust on olemas meetod QueueUserWorkItem, mis vĂ”tab vastu WaitCallback tĂŒĂŒpi delegaat, mis on sama signatuuriga kui ParametrizedThreadStart, ja edastatud parameter tĂ€idab sama funktsiooni.
ThreadPool.QueueUserWorkItem(...);
VĂ€hem tuntud meetod lĂ”imede puus RegisterWaitForSingleObject teenib eesmĂ€rki korraldada mitteblokeerivaid IO-operatsioone. Delegaati, mis on edastatud sellele meetodile, kutsutakse ĂŒles siis, kui metoodile antud WaitHandle "vabastatakse" (Released).
ThreadPool.RegisterWaitForSingleObject(...)
.NET-is on lÔime taimer, mis erineb WinFormsi/WPF taimeritest selle poolest, et selle handlerit kutsutakse vÀlja puust vÔetud lÔimes.
System.Threading.Timer
Samuti on olemas ĂŒsna eksootiline viis saata delegaat tĂ€itmiseks puust lĂ”ime â meetod BeginInvoke.
DelegateInstance.BeginInvoke
Soovin veel hetkeks peatuda funktsioonil, mille kutsumine on paljude eespool nimetatud meetodite keskmes â CreateThread Kernel32.dll Win32 API-s. On olemas viis, kuidas extern-mehhanismi abil seda funktsiooni kutsuda. Olen sellist kutsumist nĂ€inud vaid korra hirmuĂ€ratavas legacy koode nĂ€ites, ning autori motivatsioon, miks ta just niimoodi tegutses, jÀÀb mulle endiselt arusaamatuks.
Kernel32.dll CreateThread
Otsing ja silumistööd voogude ĂŒle
Teie isiklikult loodud vooge ning kĂ”iki kolmanda osapoole komponente ja .NETi basseini vooge saab vaadata Visual Studio Threadsi aknas. See aken kuvab voogude teavet vaid siis, kui rakendus on silumises ja peatumise reĆŸiimis (Break mode). Siin on mugav vaadata igasuguste voogude steke, nimesid ja prioriteete ning suunata silumist konkreetsele voogule. Threadi klassi Priority omaduse abil saab mÀÀrata voolu prioriteedi, mida OC ja CLR tĂ”lgendavad soovitusena voogude vahel protsessoriaega jagades.

Task Parallel Library
Task Parallel Library (TPL) ilmus .NET 4.0. See on nĂŒĂŒd standard ja peamine tööriist asĂŒnkroonsuse haldamiseks. Iga kood, mis kasutab vanemaid lĂ€henemisviise, loetakse legacy-ks. TPL peamine osa on klass Task nimelaos System.Threading.Tasks. Task on abstraktsioon voost. Uue C# keele versiooniga saime elegantse viisi töötamiseks Task-idega â async/await operaatorid. Need kontseptsioonid vĂ”imaldasid kirjutada asĂŒnkroonset koodi justkui oleks see lihtne ja sĂŒnkrone, vĂ”imaldades isegi inimestele, kes ei mĂ”ista voogude sisemist toimimist, kirjutada rakendusi, mis ei seisku pikaajaliste tegevuste kĂ€igus. Async/awaiti kasutamine on teema ĂŒhe vĂ”i isegi mitme artikli jaoks, kuid pĂŒĂŒan lĂŒhidalt kokku vĂ”tta selle sisu:
- async on meetodi modifikaator, mis tagastab Taski vÔi void'i.
- await on mitte-blokeeriv Task'i ootamise operaator.
Veel kordi: await operaator vabastab praegu toimiva syntaksile vastava sĂ€tte, ja kui Task lĂ”petab oma tĂ€itmise, jĂ€tkub meetodi tĂ€itmine, kui konteksti töötlus on lĂ”petatud. .NET-is on see mehhanism teostatud samamoodi nagu yield return, kui kirjutatud meetod muutub terveks klassiks, mis on oleku masin ja mida saab tĂ€ita erinevate tĂŒkkide kaupa sĂ”ltuvalt nendest olekute olukordadest. Huvitav on kirjutada mĂ”ni lihtne kood asynŃ/await kasutades, kompileerida ja vaadata koostist JetBrains dotPeek abil, kus on lubatud Compiler Generated Code.
Vaadakem ĂŒle Taski kĂ€ivitamise ja kasutamise vĂ”imalused. Allpool toodud koodi nĂ€itel loome uue ĂŒlesande, mis ei tee midagi kasulikku (Thread.Sleep(10000)), kuid tegelikus elus peaks see olema mingi keeruline CPU-d kasutav töö.
using TCO = System.Threading.Tasks.TaskCreationOptions;
public static async void VoidAsyncMethod() {
var cancellationSource = new CancellationTokenSource();
await Task.Factory.StartNew(
// Tegevuskood tÀidetakse teises kontekstis
() => Thread.Sleep(10000),
cancellationSource.Token,
TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness,
scheduler
);
// Kood pÀrast await-i tÀidetakse salvestatud kontekstis
}
Task luuakse mitmete valikute abil:
- LongRunning â vihje, et ĂŒlesanne ei toimu kiiresti, seega on mĂ”ttekas mĂ”elda sellele, et mitte vĂ”tta niĆĄit teiselt registrilt, vaid luua selle ĂŒlesande jaoks eraldi.
- AttachedToParent â Task-id vĂ”ivad paigutuda hierarhiatesse. Kui seda valikut on kasutatud, vĂ”ib Task olla seisundis, kus see on ise lĂ”petatud ja ootab alampunktide tĂ€itmist.
- PreferFairness â tĂ€hendab, et oleks hea tĂ€ita varem edastatud Task-e enne hiljem edastatud. Kuid see on vaid soovitus ja tulemus ei ole garanteeritud.
Teise parameetrina edastatakse meetodisse CancellationToken. Et Ă”igesti kĂ€sitleda operatsiooni tĂŒhistamist pĂ€rast selle kĂ€ivitamist, peaks tĂ€idetav kood olema tĂ€idetud CancellationToken tĂ”endite staatuste kontrollidega. Kui kontrollid puuduvad, siis tĂŒhistamise meetod, mis kutsutakse esile objekti CancellationTokenSource pealt, suudab Task'i tĂ€itmise peatada ainult enne selle kĂ€ivitamist.
Viimase parameetrina edastatakse objekti scheduler tĂŒĂŒp TaskScheduler. See klass ja selle alamlassi on mĂ”eldud Task'ide jaotamisstrateegiate haldamiseks, vaikimisi tĂ€idetakse Task juhuslikul niidil basseinist.
Loodud Task'ile on rakendatud ooteoperaator await, see tÀhendab, et kood, mis on kirjutatud selle jÀrel, kui see on, tÀidetakse samas kontekstis (tihti see tÀhendab, et samal niidil), mis on koodis enne await.
Meetod on mĂ€rgitud async void, see tĂ€hendab, et selles on lubatud ooteoperaatori await kasutamine, kuid kutsuv kood ei saa ootama jÀÀda selle tĂ€itmist. Kui see vĂ”imalus on vajalik, peab meetod tagastama Task. Async void mĂ€rgitud meetodeid esineb ĂŒsna sageli: reeglina on need sĂŒndmuste kĂ€sitlejad vĂ”i muud meetodid, mis töötavad pĂ”himĂ”ttel teha ja unustada (fire and forget). Kui on vajalik mitte ainult vĂ”imaldada oodata tĂ€itmise lĂ”ppu, vaid ka tagastada tulemus, tuleb kasutada Task.
Task'il, mille meetod StartNew tagastas, saab aga nagu igal teiselgi, kutsuda meetodi ConfigureAwait parameetriga false, siis jĂ€tkub tĂ€itmine pĂ€rast await mitte pĂŒĂŒdnud konteksti, vaid suvaliseks. Seda tuleb alati teha, kui koodi pĂ€rast await'i tĂ€itmise kontekst ei ole oluline. Samuti on see MS-i soovitus koodi kirjutamiseks, mis tarnitakse pakitud raamatukoguna.
VÔtame veel hetkeks aega, et peatuda sellel, kuidas oodata Task'i tÀitmise lÔppu. Allpool on koodinÀide koos kommentaaridega, millal ootus on tehtud tinglikult hÀsti ja millal 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 Task'i tÀitmise lÔpuni, blokeerimata kutsuvat niiti, tulemuse töötlemisse naaseme vaid siis, kui see on juba olemas, kuni siis on kutsuv niit iseendale usaldatud.
Teises variandis blokeerime kutsevoo, kuni meetodi tulemus on arvutatud. See on halb mitte ainult seetĂ”ttu, et oleme kasutanud voogu, mis on programmi vÀÀrtuslik ressurss, lihtsalt passiivselt, vaid ka seetĂ”ttu, et kui kutsemeetodis, mida kutsume, on await, ja sĂŒnkroniseerimise kontekst eeldab tagasi tagasiviimist kutsevoogu pĂ€rast await'i, saame deadlock'i: kutsevoog ootab, kuni asĂŒnkroonne meetod on tulemuse arvutanud, samas kui asĂŒnkroonne meetod pĂŒĂŒab asjatult jĂ€tkata oma tĂ€itmist kutsevoos.
Veel ĂŒheks selle lĂ€henemise puuduseks on keerulisem veahaldus. Asi on selles, et asĂŒnkroonse koodi vigade kĂ€sitlemine async/await abil on vĂ€ga lihtne â need kĂ€ituvad nagu oleks kood sĂŒnkroonne. Samas, kui kasutame sĂŒnkroonset ootamist Task'i puhul, originaalvĂ€ljavĂ”te pakitakse AggregateException'i, mistĂ”ttu tuleb erandi kĂ€sitlemiseks uurida InnerException'i tĂŒĂŒpi ja kirjutada ise if-ahel ĂŒhe catch-bloki sees vĂ”i kasutada catch when konstruktsiooni, selle asemel, et kasutada C# maailmas harjumuslikku catch-blokkide ahelat.
Kolmas ja viimane nÀide on samuti halb sama pÔhjuse tÔttu ja sisaldab kÔiki neid samu probleeme.
Meetodid WhenAny ja WhenAll on ÀÀrmiselt mugavad grupi Task'ide ootamiseks, need pakivad grupi Task'e ĂŒhte, mis aktiveerub kas esimesena toimiva Task'i vĂ”i siis kui kĂ”ik on lĂ”petanud oma tĂ€itmise.
Voogude peatamine
Erinevatel pĂ”hjustel vĂ”ib tekkida vajadus peatada voog pĂ€rast selle kĂ€ivitamist. Selleks on olemas mitmeid viise. Thread'i klassil on kaks meetodit sobivate nimede jĂ€rgi â see on Abort ja Interrupt. Esimene ei ole rangelt soovitatav kasutada, kuna pĂ€rast selle kutsumist visatakse juhuslikul hetkel, igasuguses kĂ€skluste töötlemises, erand ThreadAbortedException. Te ei oota, et selline erand ilmuks, kui inkrementeerite mĂ”nda tĂ€isarvu muutujaid, eks? Aga selle meetodi kasutamisel on see tĂ€iesti reaalne olukord. Kui on vajalik keelata CLR-i genereerimine selliste erandite jaoks teatud koodilĂ”igus, saab selle pakkida Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Iga kood, mis on kirjutatud finally plokki, kĂ€itub selliste vĂ€ljakutsete tĂ”ttu. SellepĂ€rast vĂ”ib raamistikust leida plokke, kus try on tĂŒhi, aga finally mitte. Microsoft ei soovita seda meetodit kasutada nii palju, et nad ei lisanud seda .net core'i.
Meetod Interrupt töötab ennustatavamalt. See vĂ”ib katkestada lĂ”ime erandiga. ThreadInterruptedException ainult siis, kui lĂ”im on ootereĆŸiimis. Sellisesse olekusse ta lĂ€heb, kui ootab WaitHandle'i, lock'i vĂ”i pĂ€rast Thread.Sleep'i kutsumist.
MĂ”lemad ĂŒlaltoodud variandid on ebamugavad oma ettearvamatuse tĂ”ttu. Lahenduseks on kasutada struktuuri CancellationToken ja klassi CancellationTokenSource. Idee on jĂ€rgmine: luuakse CancellationTokenSource klassi eksemplar ja ainult selle omanik vĂ”ib peatada operatsiooni, kutsudes vĂ€lja meetodi Cancel. Samas operatsioonis edastatakse ainult CancellationToken. CancellationToken'i omanikud ei saa ise operatsiooni tĂŒhistada, vaid saavad ainult kontrollida, kas operatsioon on tĂŒhistatud. Selleks on olemas boolean omadus IsCancellationRequested ja meetod ThrowIfCancelRequested. Viimane genereerib erandi TaskCancelledException kui CancellationTokenSource'i eksemplaris, mis oli seotud CancellationToken'iga, kutsuti vĂ€lja meetod Cancel. Just seda meetodit soovitan kasutada. See on parem kui eelnevad variandid, pakkudes tĂ€ielikku kontrolli selle ĂŒle, millal vĂ”ib operatsioonis esinev erand olla katkestatud.
KĂ”ige drastilisem variant lĂ”ime peatamiseks on Win32 API funktsiooni TerminateThread kutsumine. CLR-i kĂ€itumine pĂ€rast selle funktsiooni kutsumist vĂ”ib olla ettearvamatu. MSDN-is kirjutatakse selle funktsiooni kohta jĂ€rgmist: âTerminateThread is a dangerous function that should only be used in the most extreme cases. â
Legacy-API muutmine ĂŒlesande pĂ”histel meetoditel kasutades FromAsync meetodit.
Kui teil on vedanud töötada projektis, mis alustati pĂ€rast seda, kui ĂŒlesanded olid sisse viidud ja enam ei pĂ”hjustanud enamiku arendajate vaikset hirmu, siis ei pea te tegelema suure hulga vanade API-dega, olgu need siis kolmandate osapoolte vĂ”i teie meeskonna minevikus vĂ€lja mĂ”eldud. Ănneks on .NET Frameworki arendustiim meie eest hoolt kandnud, ehkki vĂ”ib-olla oli eesmĂ€rk hoolitseda enda eest. Igal juhul on .NET-is mitmeid tööriistu, mis vĂ”imaldavad valutult muuta vana asĂŒnkroonse programmeerimise lĂ€henemisviisi uuteks. Ăks neist on Meetod FromAsync klassis TaskFactory. Alloleva nĂ€ite puhul pakendan vanad asĂŒnkroonsed meetodid klassis WebRequest Taskâi, 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 sellist asja ei ole teil tĂ”enĂ€oliselt vaja teha sisseehitatud tĂŒĂŒpidega, kuid iga vana projekt on tĂ€is meetodeid BeginDoSomething, mis tagastavad IAsyncResult ning meetodeid EndDoSomething, mis neid vastu vĂ”tavad.
Legacy-API muutmine ĂŒlesande pĂ”histeks klassi TaskCompletionSource abil
Veel ĂŒks oluline tööriist, mida arvestada, on klass TaskCompletionSource. Funktsioonide, eesmĂ€rgi ja tööpĂ”himĂ”tte poolest vĂ”ib see meenutada klassi ThreadPool meetodit RegisterWaitForSingleObject, millest ma varem rÀÀkisin. Selle klassi abil on lihtne ja mugav vanu asĂŒnkroonseid API-sid Task'idesse pakendada.
Te ĂŒtlete, et ma olen juba rÀÀkinud klassi TaskFactory meetodist FromAsync, mis on selleks otstarbeks mĂ”eldud. Siin tuleb meelde tuletada kĂ”iki .NET asĂŒnkroonsete mudelite arengulugusid, mida Microsoft on pakunud viimase 15 aasta jooksul: enne Task-Based Asynchronous Pattern (TAP) oli olemas Asynchronous Programming Pattern (APP), mis pĂ”hines meetoditel BeginDoSomething, mis tagastavad IAsyncResult ja meetoditel EndDoSomething, mis neid vastu vĂ”tavad ning vanadele aastatele sobib suurepĂ€raselt meetod FromAsync, kuid aja jooksul asendas selle Event Based Asynchronous Pattern (EAP), mis nĂ€gi ette, et asĂŒnkroonse toimingu lĂ”petamisel kutsub esile sĂŒndmuse.
TaskCompletionSource on ideaalne lahendus legacy-API-de mĂ€hkimiseks, millel on ĂŒrituste mudel. Selle töö pĂ”hiolemus seisneb selles, et selle klassi objektil on avalik omadus tĂŒĂŒbiga Task, mille olekut saab hallata meetodite SetResult, SetException jne. kaudu. TaskCompletionSource klassi kohta. Seal, kus on kasutatud ooteoperatsiooni await selle Taskâi puhul, tĂ€idetakse see vĂ”i visatakse vĂ€lja erind, sĂ”ltuvalt sellest, millist meetodit rakendati TaskCompletionSource'ile. Kui see ei ole endiselt selge, siis vaatame seda nĂ€idet koodist, kus mĂ”ni vana API EAP ajast mĂ€hitakse Task'i abil TaskCompletionSource'iga: kui sĂŒndmus aktiveerub, muudetakse Task seisundiks Completed ja meetod, mis rakendas sellele Taskâile ooteoperatsiooni await, jĂ€tkab tĂ€itmist, saades objekti. result.
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Àhkimine ei ole ainus asi, mida TaskCompletionSource abil teha saab. Selle klassi kasutamine avab huvitavaid vÔimalusi erinevate API-de projekteerimiseks, mis pÔhinevad Task'idel, mis ei hÔivata teemasid. Ja teema, nagu me teame, on kallis ressurss ning nende arv on piiratud (peamiselt RAMi mahuga). Seda piirangut on lihtne saavutada, arendades nÀiteks koormatud veebirakendust keerulise Àriloogikaga. Uurime neid vÔimalusi, millest ma rÀÀgin, rakendades sellist trikki nagu Long-Polling.
LĂŒhidalt, triki sisu on jĂ€rgmine: peate saama API-lt teavet mĂ”nedest sĂŒndmustest, mis toimuvad sellel poolel, kuid mingil pĂ”hjusel ei saa API sĂŒndmust teatada, vaid saab ainult tagastada oleku. Sellisteks nĂ€ideteks on 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 HTTP serverilt kĂŒsida. HTTP server ei saa ise klientidega suhelda. Lihtne lahendus on serveri kĂŒsitlemine ajastuse kaudu, kuid see koormab serverit ning tekitab keskmise viivituse TimerInterval / 2. Selle mööda minemiseks leiutati trikk nimega Long Polling, mis eeldab serverilt vastuse viivitamist seni, kuni aegumise ajalimiit lĂ”ppeb vĂ”i sĂŒndmus toimub. Kui sĂŒndmus toimub, siis see töödeldakse; kui ei, siis saadetakse pĂ€ring uuesti.
while(!eventOccures && !timeoutExceeded) {
CheckTimout();
CheckEvent();
Thread.Sleep(1);
}
Kuid selline lahendus nĂ€itab end kohutavalt, kui ootavate klientide arv kasvab, kuna iga selline klient, kes ootab sĂŒndmust, kasutab ĂŒht eraldi lĂ”ime. Samuti saame lisaviivituse 1 ms sĂŒndmuse toimumisel, mis enamasti ei ole oluline, kuid miks teha tarkvara halvemaks, kui see vĂ”iks olla? Kui eemaldada Thread.Sleep(1), koormame ĂŒhte CPU tuuma 100% ulatuses, pöörledes kasutu tsĂŒkli sees. TaskCompletionSource abil saab seda koodi lihtsasti ĂŒmber kujundada ja lahendada kĂ”ik varem 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 production-ready, vaid ainult nÀidis. Reaalsetes olukordades tuleb veel vÀhemalt hallata olukord, kus sÔnum saabus hetkel, kui keegi seda ei oota: sellisel juhul peaks meetod AcceptMessageAsync tagastama juba lÔpetatud Task'i. Kui see juhtum on kÔige sagedasem, siis vÔib mÔelda ka ValueTask'i kasutusele.
KĂŒsimuse saamisel loome ja paneme sĂ”nastikku TaskCompletionSource, ning ootame, mis juhtub esimesena: kas aeg on möödas vĂ”i saadakse sĂ”num.
ValueTask: miks ja kuidas
Async/await operaatorid, nagu ka yield return operaator, genereerivad meetodist olekute masina, mis tĂ€hendab, et luuakse uus objekt. See on enamasti ebaoluline, kuid harvadel juhtudel vĂ”ib see tekitada probleeme. Selliseks juhtumiks vĂ”ib olla meetod, mida kutsutakse tĂ”eliselt sageli, kĂŒmneid ja sadu tuhandeid kordi sekundis. Kui selline meetod on kirjutatud nii, et enamikul juhtudel tagastab ta tulemuse, möödudes kĂ”igist await meetoditest, siis .NET pakub tööriista selle optimeerimiseks â ValueTask struktuur. Et seda paremini mĂ”ista, vaatame selle kasutamise nĂ€idet: meil on vahemĂ€lu, kuhu me kĂ€ime vĂ€ga sageli. MĂ”ned vÀÀrtused on seal ja siis me tagastame need otse, kui neid pole, siis lĂ€heneme mĂ”nele aeglasele IO-le nende saamiseks. Viimast soovime teha asĂŒnkroonselt, mistĂ”ttu kogu meetod osutub asĂŒnkroonseks. Seega ilmselge variant meetodi kirjutamiseks on jĂ€rgmine:
public async Task<string> GetById(int id) {
if (cache.TryGetValue(id, out string val))
return val;
return await RequestById(id);
}
Soovi korral natuke optimeerida ja vÀikese kartuse tÔttu, mida Roslyn sellega genereerib, saame seda nÀidet kirjutada jÀrgmiselt:
public Task<string> GetById(int id) {
if (cache.TryGetValue(id, out string val))
return Task.FromResult(val);
return RequestById(id);
}
Optimaalse lahendusena oleks sel juhul optimeerida kuuma tee, nimelt, saada vÀÀrtust sÔnastikust ilma liigsete allokatsioonide ja GC koormuseta, samas kui harvadel juhtudel, kui me peame minema IO-sse andmete saamiseks, jÀÀb kÔik enam-vÀhem samaks:
public ValueTask<string> GetById(int id) {
if (cache.TryGetValue(id, out string val))
return new ValueTask<string>(val);
return new ValueTask<string>(RequestById(id));
}
Vaatame seda koodifragmendi lĂ€hemalt: kui vahemĂ€lus on vÀÀrtus, loome struktuuri, vastasel juhul on tegelik ĂŒlesanne pakitud vÀÀrtuslikku. Kutsuv kood ei hooli, millisest teest see kood kĂ€idi: ValueTask kĂ€itub C# sĂŒntaksist vaadatuna samamoodi nagu tavaline Task.
TaskSchedulerâid: Taskâide kĂ€ivitamise strateegiate haldamine
jĂ€rgmine API, mida sooviksime vaadata, on klass TaskScheduler ja selle tuletised. Olen juba eespool maininud, et TPL-is on vĂ”imalik hallata ĂŒlesannete jaotamise strateegiaid lĂ”imede vahel. Sellised strateegiad mÀÀratakse ĂŒlesande ajakava klassi pĂ€rijates. Peaaegu iga strateegia, mida vĂ”ib vaja minna, leiab raamatukogust. ParallelExtensionsExtras, mille on vĂ€lja töötanud Microsoft, kuid mis ei ole osa .NET-ist ja tarnitakse Nuget paketi vormis. Vaadakem lĂŒhidalt mĂ”ningaid neist:
- CurrentThreadTaskScheduler â tĂ€idab ĂŒlesandeid praegusel lĂ”imel.
- LimitedConcurrencyLevelTaskScheduler â piirab samaaegselt tĂ€idetavate ĂŒlesannete arvu parameetriga N, mille ta vĂ”tab konstruktoris.
- OrderedTaskScheduler â mÀÀratletakse kui LimitedConcurrencyLevelTaskScheduler(1), seega tĂ€idetakse ĂŒlesanded jĂ€rjestikku.
- WorkStealingTaskScheduler â rakendab lĂ€henemist ĂŒlesannete jaotamisele. Tegelikult on see eraldi lĂ”imede bassein. See lahendab probleemi, et .NET-i lĂ”imede bassein on staatiline klass, ĂŒks kĂ”ikide rakenduste jaoks, mistĂ”ttu vĂ”ivad selle ĂŒlekoormus vĂ”i vale kasutamine ĂŒhes rakenduse osas pĂ”hjustada kĂ”rvalmĂ”jusid teises. Veelgi enam, selliste defektide pĂ”hjuse mĂ”istmine on ÀÀrmiselt keeruline. Seega vĂ”ib olla vajalik kasutada eraldi WorkStealingTaskScheduler-e rakenduse osades, kus lĂ”imede basseini kasutamine vĂ”ib olla agressiivne ja ettearvamatu.
- QueuedTaskScheduler â vĂ”imaldab tĂ€ita ĂŒlesandeid prioriteediga jĂ€rjekorra reeglite jĂ€rgi.
- ThreadPerTaskScheduler â loob iga ĂŒlesande tĂ€itmiseks eraldi lĂ”ime. See vĂ”ib olla kasulik ettearvamatult pikka aega tĂ€idetavate ĂŒlesannete jaoks.
Microsofti blogis on hea ja pĂ”hjalik teave ĂŒlesande ajakava kohta.
Kogu ĂŒlesannetega seotud vĂ”imaluste lihtsaks tĂ”rkeotsimiseks Visual Studios on olemas ĂŒlesannete aken. Selles aknas saab nĂ€ha ĂŒlesande praegust seisu ja liikuda hetkel tĂ€idetava koodi ritta.

PLinq ja klass Parallel
Lisaks Task'idele ja kĂ”igile, mis nendega seondub .NET-s, on veel kaks huvitavat tööriista, nimelt PLinq (Linq2Parallel) ja klass Parallel. Esimene lubab paralleelset tĂ€itmist kĂ”igist Linq operatsioonidest mitmel lĂ”ngal. LĂ”ngade arvu saab konfigureerida laienduse Meetodiga WithDegreeOfParallelism. Kahjuks ei piisa PLinq-lt vaikereĆŸiimis sageli teabest teie andmeallika sisust, et saavutada mĂ€rkimisvÀÀrne kiirusvĂ”it, kuid katse tegemise hind on vĂ€ga madal: peate lihtsalt enne Linq meetodite ahelat kutsuma AsParallel meetodi ja teostama jĂ”udlusteste. Veelgi enam, PLinq-le on vĂ”imalik edastada tĂ€iendavat teavet andmeallika iseloomu kohta Partitions mehhanismi abil. TĂ€iendavalt on vĂ”imalik lugeda ja .
Statiline klass Parallel pakub meetodeid kollektsiooni paralleelseks lĂ€bimiseks Foreach, For tsĂŒkli tĂ€itmiseks ja mitme delegaadi paralleelseks kĂ€ivitamiseks Invoke. Praeguse lĂ”nga tĂ€itmine peatub, kuni arvutuste lĂ”petamiseni. LĂ”ngade arvu saab konfigureerida, edastades ParallelOptions viimase argumendina. Valikute abil on samuti vĂ”imalik mÀÀrata TaskScheduler ja CancellationToken.
JĂ€reldused
Kui ma hakkasin kirjutama seda artiklit oma ettekande ja selle ajal kokku kogutud teabe pÔhjal, ei oodanud ma, et sellest tuleb nii palju. Praegu, kui tekstiredaktor, kus ma seda artiklit tippin, piidleb mulle nÀidates, et leht on juba 15. kohal, teen vahekokkuvÔtte. Teised trikid, API-d, visuaalsed tööriistad ja alajaotused kÀsitletakse jÀrgmises artiklis.
KokkuvÔtted:
- On oluline tunda tööriistu lĂ”ngade, asĂŒnkroonsuse ja paralleelsuse kĂ€sitlemiseks, et kasutada tĂ€napĂ€evaste arvutite resursse.
- .NET-is on nende eesmÀrkide saavutamiseks palju erinevaid tööriistu
- Need ei ilmunud kÔik kohe, seetÔttu vÔib sageli kohata legacy lahendusi, kuid vana API-d on vÔimalik konverteerida ilma suuremate jÔupingutusteta.
- .NET-is kÀsitletakse lÔngadega töötamist klasside Thread ja ThreadPool kaudu
- Meetodid Thread.Abort, Thread.Interrupt ja Win32 API funktsioon TerminateThread on ohtlikud ja nende kasutamist ei soovitata. Selle asemel on parem kasutada CancellationToken mehhanismi.
- Voog on vÀÀrtuslik ressurss, mille kogus on piiratud. Tuleb vĂ€ltida olukordi, kus vood ootavad sĂŒndmusi. Selleks on mugav kasutada klassi TaskCompletionSource
- KĂ”ige vĂ”imsamad ja arenenumad .NET tööriistad paralleelsuse ja asĂŒnkroonsuse jaoks on Task'id.
- C# async/await operaatorid realiseerivad mitteblokeeriva oote kontseptsiooni
- Task'ide jaotamist voogude vahel saab hallata tuletatud TaskScheduler'i klasside abil
- ValueTask struktuur vÔib olla kasulik hot-pathide ja mÀlu liikluse optimeerimisel
- Visual Studio Tasks ja Threads aknad pakuvad palju kasulikku teavet mitme töötluse vĂ”i asĂŒnkroonse koodi tĂ”rkeotsingu jaoks
- PLinq on Àge tööriist, kuid see ei pruugi teie andmeallika kohta piisavalt teavet omada, kuid seda saab parandada jagamise mehhanismi abil
- JĂ€tkubâŠ
Allikas: habr.com
