Poza në Habr origjinalin e artikullit, përkthimi i të cilit është publikuar në korporatën .
Nevoja për të bërë diçka asinkron, pa pritur rezultatin këtu dhe tani, ose për të ndarë një punë të madhe midis disa njësive që e realizojnë atë ka ekzistuar dhe para shfaqjes së kompjuterëve. Me shfaqjen e tyre, kjo nevojë u bë shumë e dukshme. Tani, në vitin 2019, duke shkruar këtë artikull në një laptop me procesor Intel Core me 8 bërthama, i cili po operon njëkohësisht me qindra procese e akoma më shumë threads. Pranë, qëndron një telefon pak i përdorur, blerë disa vite më parë, me një procesor 8-bërthamësh. Në burimet tematike ka shumë artikuj e video, ku autorët e tyre shprehin fascinatën me telefonat e avancuar të këtij viti që janë duke përdorur procesorë me 16 bërthama. MS Azure ofron një makinë virtuale me më pak se 20 $/orë me një procesor 128-bërthamësh dhe 2 TB RAM. Fatkeqësisht, nuk është e mundur të shfrytëzohet maksimumi dhe të kontrollohet kjo fuqi pa ditur si të menaxhohet ndërveprimi i threads.
Termat
Procesi (Process) â objejti i OS, hapĂ«sirĂ« e izoluar adresimi, pĂ«rmban threads.
Threadi (Thread) â objekti i OS, njĂ«si mĂ« e vogĂ«l ekzekutimi, pjesĂ« e procesit, threads ndajnĂ« memorjen dhe burimet e tjera midis tyre brenda procesit.
Multitasking â vetia e OS, mundĂ«sia pĂ«r tĂ« executuar disa procese njĂ«kohĂ«sisht
Multicore â vetia e procesorit, mundĂ«sia pĂ«r tĂ« pĂ«rdorur disa bĂ«rthama pĂ«r pĂ«rpunimin e tĂ« dhĂ«nave
Multiprocessing â vetia e kompjuterit, mundĂ«sia pĂ«r tĂ« punuar njĂ«kohĂ«sisht me disa procesorĂ« fizikisht
Multithreading â vetia e procesit, mundĂ«sia pĂ«r tĂ« shpĂ«rndarĂ« pĂ«rpunimin e tĂ« dhĂ«nave midis disa threads.
Paraleliteti â ekzekutimi i disa veprimeve fizikisht njĂ«kohĂ«sisht nĂ« njĂ« njĂ«si kohe
Asinkroniteti â ekzekutimi i njĂ« operacioni pa pritur pĂ«rfundimin e kĂ«saj pĂ«rpunimi, rezultati i ekzekutimit mund tĂ« pĂ«rpunohen mĂ« vonĂ«.
Metafora
Jo tĂ« gjitha pĂ«rkufizimet janĂ« tĂ« mira dhe disa kĂ«rkojnĂ« shpjegim tĂ« mĂ«tejshĂ«m, prandaj pĂ«r terminologjinĂ« formalisht tĂ« futur do tĂ« shtoj njĂ« metaforĂ« pĂ«r pĂ«rgatitjen e mĂ«ngjesit. PĂ«rgatitja e mĂ«ngjesit nĂ« kĂ«tĂ« metaforĂ« â process.
Duke përgatitur mëngjesin në mëngjes unë (CPU) shkoj në kuzhinë (Një kompjuter). Kam 2 duar (Bërthamat). Në kuzhinë ka një sërë pajisjesh (IO): furra, kettle, toaster, frigorifer. Unë ndizem gazin, vendos një tigan mbi të dhe derdh vaj atje, pa pritur që të nxehet (asinkronisht, Non-Blocking-IO-Wait), nxjerr vezët nga frigoriferi dhe i thyj ato në një pjate, pastaj i rrah me një dorë (Thread#1), ndërsa me dorën tjetër (Thread#2) mbaj pjatën (Burimi i Ndashur). Tani do të doja të ndiznim edhe kettle-n, por nuk kam duar të mjaftueshme (Thread Starvation) Gjatë kësaj kohe tiganit po i nxehet (Përpunimi i rezultatit) në të cilin derdh atë që kam rrahur. Unë shkoj te kettle dhe e ndizem dhe thjesht shikoj si uji në të fillon të vlojë (Blocking-IO-Wait), ndonëse mund të kisha larë pjatën ku rrahja e omletës në atë kohë.
Kam përgatitur omletën duke përdorur vetëm 2 duar, më shumë nuk kam, por në momentin e rrahjes së omletës ndodhnin menjëherë 3 operacione: rrahja e omletës, mbajtja e pjatës, ngrohja e tiganit. CPU është pjesa më e shpejtë e kompjuterit, IO është ajo që shpesh ngadalëson procesin, prandaj shpesh zgjidhja efektive është të angazhohet CPU ndërsa po merret të dhëna nga IO.
Duke vazhduar me metaforën:
- Nëse në procesin e përgatitjes së omletës, do të provoja të rihesha, do të ishte një shembull i multitasking. Një nuancë e rëndësishme: kompjuterët me këtë e kanë shumë më mirë sesa njerëzit.
- Kuzhina me disa kuzhinierĂ«, pĂ«r shembull nĂ« njĂ« restorant â kompjuter multicore.
- ShumĂ« restorante nĂ« njĂ« food court nĂ« qendrĂ«n tregtare â data center.
Mjetet .NET
Në punën me threads, ashtu si në shumë gjëra të tjera, .NET është i mirë. Me çdo version të ri, ai paraqet gjithnjë e më shumë mjete të reja për të punuar me to, shtresa të reja abstraksioni mbi threads e OS. Në punën me ndërtimin e abstraksioneve, zhvilluesit e këtij framework përdorin një qasje që lejon përdorimin e abstraksioneve të nivelit të lartë dhe kalimin një ose disa nivele më poshtë. Shpesh kjo nuk është e nevojshme, madje hap mundësinë për të hapur vetë një plumb me një armë gjuetie, por nganjëherë, në raste të rralla, mund të jetë mënyra e vetme për të zgjidhur një problem që nuk zgjidhet në nivelin aktual të abstraksionit.
Me mjete nënkuptoj si ndërfaqet programatore (API) të ofruara nga framework-u dhe paketat e jashtme, ashtu si edhe zgjidhje të tëra programore që lehtësojnë gjetjen e ndonjë problemi të lidhur me kodin shumë-folës.
Nisja e një thread-i
Klasa Thread, klasa më bazike në .NET për punën me threads. Konstruktorin merr një nga dy delegatët:
- ThreadStart â Pa pa parametra
- ParametrizedThreadStart â me njĂ« parameter tĂ« tipit object.
Delegati do të ekzekutohet në një thread të ri të krijuar pas thirrjes së metodës Start, nëse në constructor është kaluar një delegat i tipit ParametrizedThreadStart, atëherë në metodën Start duhet të kaloni një objekt. Ky mekanizëm është i nevojshëm për të kaluar çdo informacion lokal në thread. Duhet të theksohet se krijimi i një thread është një operacion i shtrenjtë, dhe vetë thread është një objekt i rëndë, sepse ndahen të paktën 1MB memorie për stack dhe kërkon ndërveprim me API-në e OS.
new Thread(...).Start(...);
Klasa ThreadPool paraqet konceptin e një pool-i. Në .NET, pool-i i threads është një arritje inxhinierike dhe zhvilluesit nga Microsoft kanë investuar shumë përpjekje që ai të funksionojë në mënyrë optimale në skenarë të ndryshëm.
Koncepcioni i përgjithshëm:
Që nga starti, aplikacioni në sfond krijon disa threads rezervë dhe ofron mundësinë për t'i përdorur ato. Nëse threads përdoren shpesh dhe në sasi të mëdha, atëherë pool-i zgjeron për të përmbushur kërkesën e kodit thirrës. Kur në pool në një moment të caktuar nuk ka threads të lirë, ai ose do të presë kthimin e një nga threads, ose do të krijojë një të ri. Nga kjo del se pool-i i threads është i përshtatshëm për veprime të shkurtra dhe i papërshtatshëm për operacione që funksionojnë si shërbime gjatë tërë kohëzgjatjes së aplikacioneve.
Për të përdorur një thread nga pool-i, ekziston metoda QueueUserWorkItem, e cila pranon një delegat të tipit WaitCallback, që për nga firma përputhet me ParametrizedThreadStart, dhe parametri që i kaloni asaj kryen të njëjtin funksion.
ThreadPool.QueueUserWorkItem(...);
Metoda më pak e njohur e pool-it të threads, RegisterWaitForSingleObject, shërben për organizimin e operacioneve IO jo bllokuese. Delegati i kaluar në këtë metodë do të thirret kur WaitHandle i kaluar në metodë të "çlirohet".
ThreadPool.RegisterWaitForSingleObject(...)
Në .NET ekziston një timer thread-i dhe ai dallon nga timer-at e WinForms/WPF sepse trajtuesi i tij do të thirret në një thread të marrë nga pool-i.
System.Threading.Timer
Po ashtu ka njĂ« mĂ«nyrĂ« mjaft ekzotike pĂ«r tĂ« dĂ«rguar njĂ« delegat pĂ«r ekzekutim nĂ« njĂ« thread nga pool-i â metoda BeginInvoke.
DelegateInstance.BeginInvoke
Dua gjithashtu tĂ« pĂ«rmend nĂ« kalim funksionin nĂ« thirrjen e tĂ« cilit pĂ«rfundon shumĂ« nga metodat e pĂ«rmendura mĂ« sipĂ«r â CreateThread nga Kernel32.dll Win32 API. Ekziston njĂ« mĂ«nyrĂ«, falĂ« mekanizmit tĂ« metodave extern, pĂ«r tĂ« thirrur kĂ«tĂ« funksion. E kam parĂ« njĂ« herĂ« kĂ«tĂ« thirrje nĂ« njĂ« shembull tmerrĂ«sisht tĂ« keq tĂ« kodit legacy, dhe motivi i autorit qĂ« e bĂ«ri kĂ«tĂ« mbetet ende njĂ« mister pĂ«r mua.
Kernel32.dll CreateThread
Shikimi dhe debuggimi i threads
Threads që ju krijoni personalisht, të gjitha komponentët e tretë dhe pool-i .NET mund të shihen në dritaren Threads të Visual Studio. Kjo dritare do të shfaqë informacionin mbi threads vetëm kur aplikacioni është në modin e debugging dhe në ndalesë (Break mode). Këtu mund të shikoni lehtësisht stek, emrat dhe prioritetet e çdo thread-i, si dhe të kaloni debugging në një thread të caktuar. Pjesa Priority e klasës Thread mund të përcaktojë prioritetin e thread-it, i cili do të percepitohet si një rekomandim nga OC dhe CLR gjatë ndarjes së kohës së procesorit mes threads.

Task Parallel Library
Task Parallel Library (TPL) u shfaq nĂ« .NET 4.0. Tani kjo Ă«shtĂ« standard dhe mjeti kryesor pĂ«r punĂ« me asinkroninĂ«. Ădo kod qĂ« pĂ«rdor qasje mĂ« tĂ« vjetra konsiderohet legacy. NjĂ«sia kryesore e TPL Ă«shtĂ« klasa Task nga hapĂ«sira emri System.Threading.Tasks. Task pĂ«rfaqĂ«son njĂ« abstraksion mbi thread-in. Me versionin e ri tĂ« gjuhĂ«s C#, ne morĂ«m njĂ« mĂ«nyrĂ« elegante pĂ«r tĂ« punuar me Task-at â operatorĂ«t async/await. KĂ«to koncepte lejuan qĂ« tĂ« shkruhej kodi asinkron siç do tĂ« ishte thjesht dhe sinkron, kjo i dha mundĂ«si madje edhe njerĂ«zve qĂ« kanĂ« njohuri tĂ« pakta pĂ«r funksionet e brendshme tĂ« threads tĂ« shkruajnĂ« aplikacione qĂ« i pĂ«rdorin, aplikacione qĂ« nuk ngadalĂ«sohen gjatĂ« kryerjes sĂ« operacioneve tĂ« gjata. PĂ«rdorimi i async/await Ă«shtĂ« njĂ« temĂ« pĂ«r njĂ« ose madje disa artikuj, por do tĂ« pĂ«rpiqem tĂ« pĂ«rmbledh thelbin nĂ« disa propozita:
- async është një modifikator metode që kthehet Task ose void
- ndërsa await është operator i pritjes jo bllokuese të Task.
Përsërit: operatori await, në përgjithësi (ka përjashtime), do ta lërë rrjedhën aktuale të ekzekutimit të vazhdojë, dhe kur Task të përfundojë, rrjedha (në të vërtetë është më e saktë të thuash konteksti, por do flasim më vonë) do të jetë e lirë dhe do të vazhdojë ekzekutimin e metodës më tej. Brenda .NET ky mekanizëm është realizuar ashtu si yield return, kur metoda e shkruar shndërrohet në një klasë të tërë, e cila është një makinë gjendjesh dhe mund të ekzekutohet në pjesë të veçanta në varësi të këtyre gjendjeve. Kushdo që ka interes mund të shkruajë ndonjë kod të thjeshtë duke përdorur async/await, ta kompjutojë dhe ta shohë grumbullin me ndihmën e JetBrains dotPeek me kodin e gjeneruar nga kompajleri të aktivizuar.
Le të shqyrtojmë opsionet e nisjes dhe përdorimit të Task. Në shembullin e kodit më poshtë, ne krijojmë një tasik të ri, i cili nuk bën asgjë të dobishme (Thread.Sleep(10000)), por në jetën reale kjo duhet të jetë një punë e ndërlikuar që angazhon CPU-në.
using TCO = System.Threading.Tasks.TaskCreationOptions;
public static async void VoidAsyncMethod() {
var cancellationSource = new CancellationTokenSource();
await Task.Factory.StartNew(
// Kodi i veprimit do të ekzekutohet në kontekstin tjetër
() => Thread.Sleep(10000),
cancellationSource.Token,
TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness,
scheduler
);
// Kodi pas await do të ekzekutohet në kontekstin e kapur
}
Task krijohet me një sërë opsionesh:
- LongRunning â njĂ« tregues se detyra nuk do tĂ« pĂ«rfundojĂ« shpejt, dhe prandaj, ndoshta Ă«shtĂ« e mençur tĂ« mos merrni njĂ« rrjedhĂ« nga pool-i, por tĂ« krijoni njĂ« tĂ« veçantĂ« pĂ«r kĂ«tĂ« Task, nĂ« mĂ«nyrĂ« qĂ« tĂ« mos dĂ«mtoni tĂ« tjerĂ«t.
- AttachedToParent â Task mund tĂ« renditen nĂ« hierarki. NĂ«se Ă«shtĂ« pĂ«rdorur kjo mundĂ«si, atĂ«herĂ« Task mund tĂ« jetĂ« nĂ« njĂ« gjendje ku e vetme ka pĂ«rfunduar dhe po pret pĂ«rfundimin e atyre qĂ« janĂ« mĂ« poshtĂ«.
- PreferFairness â do tĂ« thotĂ« qĂ« do tĂ« ishte mirĂ« tĂ« ekzekutoheshin Task qĂ« janĂ« dĂ«rguar pĂ«r tu realizuar mĂ« herĂ«t para atyre qĂ« janĂ« dĂ«rguar mĂ« vonĂ«. Por kjo Ă«shtĂ« vetĂ«m njĂ« rekomandim dhe rezultati nuk Ă«shtĂ« i garantuar.
Parametri i dytë në metodë është një CancellationToken. Për të trajtuar saktësisht anulimin e operacionit pas nisjes, kodi i ekzekutuar duhet të përmbajë verifikime të gjendjes së CancellationToken. Nëse nuk ka verifikimesh, metoda Cancel që thirret në objektin CancellationTokenSource do të jetë në gjendje të ndalë ekzekutimin e Task vetëm deri në nisjen e saj.
Parametri i fundit është objekti scheduler i tipit TaskScheduler. Kjo klasë dhe pasardhësit e saj janë të destinuara për të menaxhuar strategjitë e shpërndarjes së Task nëpër rrjedha, në mënyrë që Task do të ekzekutohet në një rrjedhë të rastësishme nga pool-i.
Task i krijuar është aplikuar me operatorin await, ku do të thotë që kodi i shkruar pas tij, nëse ekziston, do të ekzekutohet në të njëjtin kontekst (shpesh kjo do të thotë se në të njëjtën rrjedhë) si kodi para await.
Metoda është markuar si async void, që do të thotë se është e lejuar përdorimi i operatorit await, por kodi i thirrjes nuk do të mund të presë përfundimin. Nëse një mundësi e tillë është e nevojshme, atëherë metoda duhet të kthejë Task. Metodat e markuara si async void janë të zakonshme: zakonisht janë menaxhues të ngjarjeve ose metoda të tjera që funksionojnë sipas parimit të ekzekutimit dhe harrojnë (fire and forget). Nëse është e nevojshme jo vetëm që të lejojë të pritet përfundimi i ekzekutimit, por edhe të kthejë rezultat, atëherë duhet të përdoret Task.
Për Task-in që kthen metoda StartNew, ashtu si për çdo Task tjetër, mund të thirret metoda ConfigureAwait me parametrin false, atëherë ekzekutimi pas await do të vazhdojë jo në kontekstin e kapur, por në një të rastësishëm. Kjo duhet bërë gjithmonë, kur për kodin pas await konteksti i ekzekutimit nuk është i rëndësishëm. Kjo gjithashtu është një rekomandim nga MS gjatë shkrimit të kodit që do të ofrohet në format të paketuar në një libër.
Le të ndalemi pak më shumë mbi mënyrën se si mund të presim për përfundimin e ekzekutimit të një Task. Më poshtë është një shembull kodi, me komente, kur pritja është bërë në mënyrë relative mirë dhe kur është bërë në mënyrë relative keq.
public static async void AnotherMethod() {
int result = await AsyncMethod(); // mirë
result = AsyncMethod().Result; // keq
AsyncMethod().Wait(); // keq
IEnumerable tasks = new Task[] {
AsyncMethod(), OtherAsyncMethod()
};
await Task.WhenAll(tasks); // mirë
await Task.WhenAny(tasks); // mirë
Task.WaitAll(tasks.ToArray()); // keq
}
Në shembullin e parë ne presim për ekzekutimin e Task pa bllokuar rrjedhën që e thirri, dhe do të kthehemi për të trajtuar rezultatin vetëm kur ai të jetë aty, deri atëherë rrjedha që e thirri është e lirë.
Në variantin e dytë, ne bllokojmë rrjedhën që thërret derisa rezultati i metodës të llogaritet. Kjo është e këqijë jo vetëm sepse kemi marrë një rrjedhë, një burim aq të çmuar të programit, me një punë të thjeshtë, por gjithashtu sepse nëse në kodin e metodës që thërrasim ka një await, dhe konteksti i sinkronizimit supozon rikthimin në rrjedhën e thirrjes pas await, atëherë do të përballemi me një deadlock: rrjedha që thërret pret derisa rezultati i metodës asinkrone të jetë llogaritur, ndërsa metoda asinkrone përpiqet të vazhdojë ekzekutimin e saj në rrjedhën e thirrjes.
NjĂ« tjetĂ«r disavantazh i kĂ«tij qasjeje Ă«shtĂ« pĂ«rpunimi mĂ« i komplikuar i gabimeve. GjĂ«ja Ă«shtĂ« se gabimet nĂ« kodin asinkron kur pĂ«rdoren async/await Ă«shtĂ« shumĂ« e lehtĂ« pĂ«r tâu trajtuar - ata sillen ashtu siç do ishte nĂ«se kodi ishte sinkron. NdĂ«rsa, nĂ«se ne aplikojmĂ« esperancĂ«n sinkrone nĂ« Task, pĂ«rjashtimi origjinal mbĂ«shtillet nĂ« AggregateException, kĂ«shtu qĂ« pĂ«r tĂ« trajtuar pĂ«rjashtimin do t'ju duhet tĂ« shqyrtoni llojin e InnerException dhe vetĂ« tĂ« shkruani njĂ« zinxhir if brenda njĂ« blloku catch ose tĂ« pĂ«rdorni konstrukcioni catch when, nĂ« vend tĂ« zinxhirĂ«ve mĂ« tĂ« zakonshĂ«m tĂ« bllokut catch nĂ« botĂ«n C#.
Të tretat dhe të fundit shembuj gjithashtu shënohen si të këqij për të njëjtën arsye dhe përmbajnë të njëjtat probleme.
Metodat WhenAny dhe WhenAll janĂ« shumĂ« tĂ« dobishme pĂ«r tĂ« pritur njĂ« grup Taskâash, ato mbĂ«shtjellin grupin e Taskâave nĂ« njĂ« tĂ« vetme, qĂ« do tĂ« aktivizohet ose nga aktivizimi i parĂ« tĂ« njĂ« Taskâi nĂ« grup, ose kur tĂ« pĂ«rfundojĂ« ekzekutimi i tĂ« gjithĂ«ve.
Ndalesa e rrjedhave
Për shkaqe të ndryshme, mund të shfaqet nevoja për të ndaluar një rrjedhë pas nisjes së saj. Për këtë ekzistojnë disa mënyra. Klasi Thread ka dy metoda me emra të përshtatshëm - këto janë Abort dhe Interrupt. E para është shumë e papreferuar për t'u përdorur, sepse pas thirrjes së saj në çdo rast, në procesin e ekzekutimit të çdo instruksioni, do të hidhet një përjashtim ThreadAbortedException. A nuk e prisni se një përjashtim i tillë do të ndodhte gjatë inkrementimit të ndonjë variabli të plotë, apo jo? E megjithatë, me përdorimin e kësaj metode, kjo është një situatë shumë e mundshme. Në rastin e nevojës për të ndaluar CLR-në gjenerimin e një përjashtimi të tillë në një pjesë të caktuar të kodit, mund të mbështillet në thirrjet Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Këto thirrje mbështjellin çdo kod të shkruar në blokun finally. Për këtë arsye, brenda kodit të kornizës mund të gjeni blloqe me një try të zbrazët, por me një finally jo të zbrazët. Microsoft e rekomandon aq shumë mos përdorimin e kësaj metode, saqë nuk e përfshiu atë në .net core.
Metoda Interrupt funksionon më parashikueshëm. Ajo mund të ndërpresë një rrjedhë me një përjashtim ThreadInterruptedException në momentet kur rrjedha është në gjendje pritjeje. Në një gjendje të tillë, ajo kalon duke pritur në WaitHandle, lock ose pas thirrjes së Thread.Sleep.
TĂ« dy variantet e pĂ«rshkruara mĂ« sipĂ«r, janĂ« tĂ« kĂ«qij pĂ«r shkak tĂ« paqartĂ«sisĂ« sĂ« tyre. Zgjidhja Ă«shtĂ« pĂ«rdorimi i strukturĂ«s CancellationToken dhe klasĂ«s CancellationTokenSource. Thelbi Ă«shtĂ« si nĂ« vijim: krijohet njĂ« instancĂ« e klasĂ«s CancellationTokenSource dhe vetĂ«m ai qĂ« e zotĂ«ron mund ta ndalĂ« operacionin duke thirrur metodĂ«n Cancel. NĂ« vetĂ« operacionin kalon vetĂ«m CancellationToken. ZotĂ«ruesit e CancellationToken nuk mund tĂ« ndalin vetĂ« operacionin, por mund vetĂ«m tĂ« kontrollojnĂ« nĂ«se operacioni Ă«shtĂ« ndaluar. PĂ«r kĂ«tĂ«, ekziston njĂ« veti boolean IsCancellationRequested dhe metodĂ«n ThrowIfCancelRequested. E fundit do tĂ« gjenerojĂ« njĂ« pĂ«rjashtim TaskCancelledException nĂ«se nĂ« instancĂ«n e naftĂ«s CancellationToken tĂ« CancellationTokenSource Ă«shtĂ« thirrur metoda Cancel. Dhe kjo Ă«shtĂ« metoda qĂ« unĂ« rekomandoj pĂ«r tâu pĂ«rdorur. Kjo Ă«shtĂ« mĂ« e mirĂ« se variantet e mĂ«parshme, duke garantuar kontroll tĂ« plotĂ« mbi momentet kur operacioni mund tĂ« ndĂ«rpritet.
Varianti mĂ« ekstrem pĂ«r ndalimin e njĂ« rrjedhe, Ă«shtĂ« thirrja e funksionit TerminateThread tĂ« Win32 API. Comportimi i CLR-sĂ« pas thirrjes sĂ« kĂ«saj funksioni mund tĂ« jetĂ« i paqartĂ«. NĂ« MSDN pĂ«r kĂ«tĂ« funksion Ă«shtĂ« shkruar si mĂ« poshtĂ«: âTerminateThread Ă«shtĂ« njĂ« funksion i rrezikshĂ«m qĂ« duhet tĂ« pĂ«rdoret vetĂ«m nĂ« rastet mĂ« ekstreme.â
Transformimi i legacy-API në Task Based me metodën FromAsync
NĂ«se ju ka rĂ«nĂ« nĂ« fat tĂ« punoni nĂ« njĂ« projekt qĂ« Ă«shtĂ« nisur pas hyrjes sĂ« Taskâave dhe ka ndaluar tĂ« shkaktojĂ« frikĂ« pĂ«r shumicĂ«n e zhvilluesve, atĂ«herĂ« nuk do t'ju duhet tĂ« trajtoni me njĂ« numĂ«r tĂ« madh tĂ« API-ve tĂ« vjetra, si ato tĂ« jashtme, ashtu edhe ato tĂ« krijuara nga ekipi juaj nĂ« tĂ« kaluarĂ«n. PĂ«r fat tĂ« mirĂ«, ekipi i zhvillimit tĂ« .NET Framework ka menduar pĂ«r ne, ndoshta me qĂ«llimin pĂ«r tĂ« ndihmuar veten. MegjithatĂ«, Ă«shtĂ« duke u dhĂ«nĂ« nĂ« .NET njĂ« numĂ«r mjetesh pĂ«r tĂ« transformuar kodin e shkruar nĂ« qasje tĂ« vjetra tĂ« programimit asinkron nĂ« njĂ« tĂ« re. NjĂ« nga to Ă«shtĂ« metoda FromAsync e TaskFactory. NĂ« shembullin e kodit mĂ« poshtĂ«, unĂ« mbĂ«shtjell metodat e vjetra asinkrone tĂ« klasĂ«s WebRequest nĂ« Task duke pĂ«rdorur kĂ«tĂ« metodĂ«.
gjendja e objektit = null;
WebRequest wr = WebRequest.CreateHttp("http://github.com");
await Task.Factory.FromAsync(
wr.BeginGetResponse,
we.EndGetResponse
);
Ky është thjesht një shembull dhe ndoshta nuk do t'ju duhet ta bëni këtë me tipet e ndërtuara, por çdo projekt i vjetër është plot me metoda BeginDoSomething që kthejnë IAsyncResult dhe metoda EndDoSomething që i pranojnë ato.
Transformimi i API-ve të vjetra në një model të bazuar në Task me ndihmën e klasës TaskCompletionSource
Një tjetër mjet i rëndësishëm për t'u shqyrtuar është klasa TaskCompletionSource. Në funksionet, qëllimin dhe parimin e punës, ai mund t'i ngjajë disi metodës RegisterWaitForSingleObject të klasës ThreadPool për të cilën kam folur më lart. Me ndihmën e kësaj klase, mund të mbështesim lehtësisht API-të e vjetra asinkrone në Task.
Do të thoni se tashmë kam folur për metodën FromAsync të klasës TaskFactory, e cila është e destinuar për këto qëllime. Këtu do të duhet të kujtojmë të gjithë historinë e zhvillimit të modeleve asinkrone në .net që ofroi microsoft në 15 vitet e fundit: para Task-Based Asynchronous Pattern (TAP) ekzistonin Asynchronous Programming Pattern (APP), i cili kishte të bënte me metodat BeginDoSomething që kthen IAsyncResult dhe metodat EndDoSomething që e pranon dhe për këto të vjetra të kohëve, metoda FromAsync është e përshtatshme, por me kalimin e kohës, për të zëvendësuar atë erdhi Event Based Asynchronous Pattern (EAP), i cili parashikonte se pas përfundimit të një operacioni asinkron do të thirrej një ngjarje.
TaskCompletionSource është veçanërisht e përshtatshme për mbështetje në Task-e të API-ve të vjetra të ndërtuara rreth modelit të ngjarjeve. Thelbi i punës së tij është si vijon: objekti i kësaj klase ka një pronë publike të tipit Task, e cila mund të kontrollohet përmes metodave SetResult, SetException etj. Klasa TaskCompletionSource. Në vendet ku u përdor operatori await në këtë Task, ai do të përfundojë ose do të dështojë me një përjashtim në varësi të metodës së aplikuar në TaskCompletionSource. Nëse ende nuk është e qartë, le të shohim këtë shembull kodimi, ku një API e vjetër nga koha e EAP mbështetet në një Task me ndihmën e TaskCompletionSource: kur ngjarja ndodh, Task do të kalojë në gjendjen e përfunduar dhe metoda që aplicoi operatorin await në këtë Task do të rinisë ekzekutimin, duke marrë objektin result.
public static Task DoAsync(this SomeApiInstance someApiObj) {
var completionSource = new TaskCompletionSource();
someApiObj.Done +=
result => completionSource.SetResult(result);
someApiObj.Do();
return completionSource.Task;
}
Këshilla & Truket e TaskCompletionSource
Mbështetja e API-ve të vjetra nuk është gjithçka që mund të bëhet me TaskCompletionSource. Përdorimi i kësaj klase hap mundësi interesante për projektimin e API-ve të ndryshme, të bazuara në Task, që nuk konsumojnë rrjedha. Dhe rrjedha, siç e dimë, është një burim i shtrenjtë dhe numri i tyre është i kufizuar (në përputhje me kapacitetin e RAM-it). Ky kufizim mund të arrihet lehtësisht duke zhvilluar, për shembull, një aplikacion web të ngarkuar me logjikë komplekse biznesi. Le të shqyrtojmë ato mundësi për të cilat po flas mbi realizimin e një truku të tillë si Long-Polling.
Nëse e përmbledhim, thelbi i trukut është ky: ju duhet të merrni informacion nga API në lidhje me disa ngjarje që ndodhin në anën e tij, përderisa API për disa arsye nuk mund të njoftojë për ngjarjen, por mund vetëm të kthejë gjendjen. Shembuj të tillë janë të gjithë API-të të ndërtuara mbi HTTP deri në kohën e WebSocket ose në rastin kur nuk mund të përdoret kjo teknologji. Klienti mund të pyesë serverin HTTP. Serveri HTTP nuk mund të inicijojë komunikimin me klientin. Një zgjidhje e thjeshtë është ndihma në këndvështrim me serverin, por kjo shkakton ngarkesë shtesë për serverin dhe një vonesë mesatare në TimerInterval / 2. Për ta anashkaluar këtë, u shpik truku i quajtur Long Polling, i cili parashikon një vonesë në përgjigjen nga serveri deri në skadimin e Timeout ose deri në ndodhin ngjarja. Nëse ndodhi ngjarja, ajo do të trajtohet; nëse jo, kërkesa do të dërgohet përsëri.
while(!eventOccurs && !timeoutExceeded) {
CheckTimeout();
CheckEvent();
Thread.Sleep(1);
}
Por një zgjidhje e tillë do të tregojë vështirësi, sapo numri i klientëve që presin ngjarjen të rritet, pasi çdo klient i tillë në pritje të një ngjarjeje zë një tërë rrjedhë. Po ashtu, shkaktojmë një vonesë shtesë prej 1ms në ndodhin e ngjarjes, shpesh kjo nuk është ndjesore, por pse ta bëjmë softuerin më të keq se çfarë mund të jetë? Nëse heqim Thread.Sleep(1), atëherë do të ngarkojmë një bërthamë të procesorit në 100% kot, duke u rrotulluar në një cikël të panevojshëm. Me ndihmën e TaskCompletionSource, mund të riformuloj këtë kod dhe të zgjidh çdo problem të shkruar më lart:
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);
}
}
Ky këtij kodi nuk i është gati për prodhim, por është vetëm një demonstrim. Për ta përdorur në raste reale, duhet të trajtohet situata kur mesazh vjen në një moment kur askush nuk e pret atë: në këtë rast, metoda AsseptMessageAsync duhet të kthejë një Task tashmë të përfunduar. Nëse ky rast është më i shpeshti, mund të mendojmë për përdorimin e ValueTask.
Kur marrim një kërkesë për një mesazh, krijojmë dhe vendosim në fjalor TaskCompletionSource, dhe më pas presim të ndodhë gjithçka: të skadojë intervali i caktuar ose të marrëim mesazhin.
ValueTask: përse dhe si
OperatorĂ«t async/await, si dhe operatori yield return, krijojnĂ« nga metoda njĂ« makinĂ« gjendjesh, dhe kjo Ă«shtĂ« krijimi i njĂ« objekti tĂ« ri, qĂ« pothuajse gjithmonĂ« nuk ka rĂ«ndĂ«si, por nĂ« raste tĂ« rralla mund tĂ« krijojĂ« probleme. Ky rast mund tĂ« jetĂ« njĂ« metodĂ« qĂ« thirret me tĂ« vĂ«rtetĂ« shpesh, pĂ«r tĂ« cilĂ«n flitet pĂ«r dhjetĂ«ra dhe qindra mijĂ«ra thirrje nĂ« sekondĂ«. NĂ«se njĂ« metodĂ« e tillĂ« Ă«shtĂ« shkruar nĂ« mĂ«nyrĂ« qĂ« nĂ« shumicĂ«n e rasteve tĂ« kthejĂ« rezultatin duke shmangur tĂ« gjitha metodat await, .NET ofron njĂ« mjet pĂ«r ta optimizuar atĂ« â struktura ValueTask. PĂ«r ta bĂ«rĂ« tĂ« qartĂ«, le tĂ« shikojmĂ« njĂ« shembull tĂ« pĂ«rdorimit tĂ« saj: ka njĂ« cache nĂ« tĂ« cilin hyjmĂ« shumĂ« shpesh. Disa vlera atje janĂ«, dhe atĂ«herĂ« ne thjesht i kthejmĂ« ato; nĂ«se jo, shkojmĂ« pĂ«r to nĂ« ndonjĂ« IO tĂ« ngadalshĂ«m. E fundit duam ta bĂ«jmĂ« asinkrone, dhe pra e gjithĂ« metoda bĂ«het asinkrone. KĂ«shtu, njĂ« variant i dukshĂ«m pĂ«r tĂ« shkruar metodĂ«n do tĂ« ishte:
public async Task GetById(int id) {
if (cache.TryGetValue(id, out string val))
return val;
return await RequestById(id);
}
Për shkak të dëshirës për të optimizuar pak, dhe frikës së vogël se çfarë do të gjenerojë Roslyn duke kompiluar këtë kod, mund ta ri-shkruajmë këtë shembull si më poshtë:
public Task GetById(int id) {
if (cache.TryGetValue(id, out string val))
return Task.FromResult(val);
return RequestById(id);
}
Zgjedhja më optimale në këtë rast do të ishte optimizimi i hot-path, dëshira për të marrë vlerën nga fjalori krejt pa alokime të panevojshme dhe ngarkesë në GC, ndërsa në ato raste të rralla kur në të vërtetë na nevojitet të shkojmë në IO për të dhëna gjithçka do të mbetet plus/minus si më parë:
public ValueTask GetById(int id) {
if (cache.TryGetValue(id, out string val))
return new ValueTask(val);
return new ValueTask(RequestById(id));
}
Le të shqyrtojmë më në detaje këtë fragment të kodit: kur ka një vlerë në cache, krijojmë një strukturë; përndryshe, realisht, një task do të jetë i rrethuar me një të rëndësishme. Kodi që thërret nuk ka rëndësi se cila rrugë është ndjekur për të ekzekutuar këtë kod: ValueTask nga këndvështrimi i sintaksës C# do të sillet ashtu si një Task i zakonshëm në këtë rast.
TaskSchedulerâĂ«t: menaxhimi i strategjive tĂ« ekzekutimit tĂ« TaskâĂ«ve
API-ja e ardhshme qĂ« do tĂ« doja tĂ« shqyrtoja Ă«shtĂ« klasa TaskScheduler dhe derivatet e tij. UnĂ« e pĂ«rmenda mĂ« lart se nĂ« TPL ka mundĂ«sinĂ« pĂ«r tĂ« menaxhuar strategjitĂ« e shpĂ«rndarjes sĂ« TaskâĂ«ve nĂ«pĂ«r tela. TĂ« tilla strategji pĂ«rcaktohen nĂ« trashĂ«gimtarĂ«t e klasĂ«s TaskScheduler. Praktikisht çdo strategji qĂ« mund tĂ« nevojitet do tĂ« gjendet nĂ« bibliotekĂ«n ParallelExtensionsExtras, tĂ« zhvilluar nga microsoft, por jo pjesĂ« e .NET, dhe ofrohen nĂ« formĂ«n e njĂ« pakete Nuget. Le tĂ« shikojmĂ« shkurtimisht disa nga to:
- CurrentThreadTaskScheduler â ekzekuton TaskâĂ«t nĂ« telin aktual
- LimitedConcurrencyLevelTaskScheduler â kufizon numrin e TaskâĂ«ve qĂ« ekzekutohen nĂ« tĂ« njĂ«jtĂ«n kohĂ« me parametrin N, qĂ« pranohet nĂ« konstruktor
- OrderedTaskScheduler â pĂ«rcaktohet si LimitedConcurrencyLevelTaskScheduler(1), kĂ«shtu qĂ« detyrat do tĂ« ekzekutohen njĂ«ra pas tjetrĂ«s.
- WorkStealingTaskScheduler â implementon pĂ«r tĂ« shpĂ«rndarĂ« detyrat. NĂ« thelb Ă«shtĂ« njĂ« ThreadPool i veçantĂ«. Zgjidh problemin qĂ« nĂ« .NET ThreadPool Ă«shtĂ« njĂ« klasĂ« statike, njĂ« pĂ«r tĂ« gjitha aplikacionet, qĂ« do tĂ« thotĂ« se ngarkimi i saj ose pĂ«rdorimi i gabuar nĂ« njĂ« pjesĂ« tĂ« programit mund tĂ« sjellĂ« efekte anĂ«sore nĂ« njĂ« tjetĂ«r. PĂ«r mĂ« tepĂ«r, tĂ« kuptosh shkakun e kĂ«tyre defekteve Ă«shtĂ« jashtĂ«zakonisht e vĂ«shtirĂ«. Pra, mund tĂ« ekzistojĂ« nevoja pĂ«r tĂ« pĂ«rdorur WorkStealingTaskScheduler tĂ« veçantĂ« nĂ« ato pjesĂ« tĂ« programit ku pĂ«rdorimi i ThreadPool mund tĂ« jetĂ« agresiv dhe i paarritshĂ«m.
- QueuedTaskScheduler â lejon ekzekutimin e detyrave sipas rregullave tĂ« rendit me prioritet
- ThreadPerTaskScheduler â krijon njĂ« tel tĂ« veçantĂ« pĂ«r çdo Task qĂ« ekzekutohet mbi tĂ«. Mund tĂ« jetĂ« e dobishme pĂ«r detyrat qĂ« ekzekutohen pĂ«r njĂ« kohĂ« tĂ« paparashikueshme tĂ« gjatĂ«.
Ka njĂ« pĂ«rmbledhje tĂ« shkĂ«lqyer rreth TaskSchedulerâĂ«ve nĂ« blogun e microsoft.
PĂ«r debugging tĂ« lehtĂ« tĂ« gjithçkaje qĂ« lidhet me TaskâĂ«t nĂ« Visual Studio ekziston dritarja Tasks. NĂ« kĂ«tĂ« dritare mund tĂ« shihni gjendjen aktuale tĂ« detyrĂ«s dhe tĂ« kaloni nĂ« linjĂ«n aktuale tĂ« kodit qĂ« po ekzekutohet.

PLinq dhe klasa Paralel
PĂ«rveç Taskâave dhe gjithçkaje tĂ« lidhur me to nĂ« .NET, ka edhe dy mjete interesante: PLinq (Linq2Parallel) dhe klasa Parallel. E para ofron ekzekutimin paralel tĂ« tĂ« gjitha operacioneve Linq nĂ« disa thretha. Numri i thretheve mund tĂ« konfigurohet me metodĂ«n e zgjerimit WithDegreeOfParallelism. FatkeqĂ«sisht, mĂ« sĂ« shpeshti PLinq nĂ« mĂ«nyrĂ«n e tij tĂ« funksionimit tĂ« paracaktuar nuk ka informacionin e mjaftueshĂ«m pĂ«r thelbin e burimit tuaj tĂ« tĂ« dhĂ«nave pĂ«r tĂ« siguruar njĂ« fitim tĂ« rĂ«ndĂ«sishĂ«m nĂ« shpejtĂ«si, nga ana tjetĂ«r, kostoja e pĂ«rpjekjes Ă«shtĂ« shumĂ« e ulĂ«t: thjesht duhet tĂ« thĂ«rrasĂ«sh metodĂ«n AsParallel para zinxhirit tĂ« metodave Linq dhe tĂ« kryesh teste pĂ«r performancĂ«n. PĂ«r mĂ« tepĂ«r, ka mundĂ«sinĂ« e kalimit tĂ« informacionit shtesĂ« nĂ« PLinq nĂ« lidhje me natyrĂ«n e burimit tuaj tĂ« tĂ« dhĂ«nave duke pĂ«rdorur mekanizmin e Partitions. Mund tĂ« lexoni mĂ« shumĂ« pĂ«r kĂ«tĂ«. dhe .
Klasa statike Parallel ofron metoda për iterimin paralel të koleksioneve me Foreach, ekzekutimin e ciklit For dhe ekzekutimin e disa delegatëve në Invoke paralel. Ekzekutimi i threht aktual do të ndalet deri sa të përfundojë ekzekutimi i llogaritjeve. Numri i thretheve mund të konfigurohet duke kaluar ParallelOptions si argumentin e fundit. Me ndihmën e opsioneve gjithashtu mund të përcaktoni TaskScheduler dhe CancellationToken.
Përfundimet
Kur fillova të shkruaj këtë artikull mbi materialet e prezantimit tim dhe informacionin që kam mbledhur gjatë punës sime pas tij, nuk e prisja që do të dilte kaq shumë. Tani, kur redaktori i tekstit në të cilin po shkruaj këtë artikull të më inspektojë se po shkon në faqen e 15-të, do të bëj një përmbledhje të përkohshme. Truket e tjera, API, veglat vizuale dhe pengesat do të trajtohen në artikullin e ardhshëm.
Konkluzione:
- Duhet të njihni mjetet e punës me threthe, asinkroninë dhe paralelizmin për të shfrytëzuar burimet e PC-ve modernë.
- .NET ofron shumë mjete të ndryshme për këto qëllime.
- Jo të gjitha ato u shfaqën menjëherë, prandaj shpesh mund të gjeni legacy, megjithatë ka mënyra për të transformuar API-të e vjetra pa shumë mundim.
- Puna me threthe në .NET paraqitet përmes klasave Thread dhe ThreadPool.
- Metodat Thread.Abort, Thread.Interrupt, funksioni Win32 API TerminateThread janë të rrezikshme dhe nuk rekomandohen për t'u përdorur. Në vend të tyre, është më mirë të përdorni mekanizmin e CancellationToken.
- Threthi është një burim i çmuar, numri i tyre është i kufizuar. Duhet të shmangni situatat kur threthat janë të zënë duke pritur ngjarje. Për këtë, është e përshtatshme të përdorni klasën TaskCompletionSource.
- Mjeti mĂ« i fuqishĂ«m dhe i avancuar i .NET pĂ«r punĂ«n me paralelizmin dhe asinkroninĂ« janĂ« TaskâĂ«t.
- Operatorët c# async/await zbatojnë konceptin e pritjes jo bllokuese.
- Menaxhimi i shpĂ«rndarjes sĂ« TaskâĂ«ve midis thretheve mund tĂ« bĂ«het pĂ«rmes klasave tĂ« nxjerra nga TaskScheduler.
- Struktura ValueTask mund të jetë e dobishme për optimizimin e hot-paths dhe të trafikut të memories.
- Dritaret Tasks dhe Threads në Visual Studio ofrojnë shumë informacion të dobishëm për debuggimin e kodit shumëthrethor ose asinkron.
- PLinq është një mjet i shkëlqyer, por mund të mos ketë informacion të mjaftueshëm për burimin tuaj të të dhënave, përveç kësaj, kjo mund të parandalohet me mekanizmin e ndarjes.
- VazhdonâŠ
Burimi: habr.com
