.NET: Multithreading eta asinkronia lantzeko tresnak. 1. zatia

Habr-en jatorrizko artikulua argitaratzen ari naiz, eta horren itzulpena korporatiboan argitaratuta dago blog post.

Zerbait modu asinkronoan egin beharra, hemen eta orain emaitzari itxaron gabe, edo lan handia egiten ari diren hainbat unitateren artean banatzeko beharra, ordenagailuen agerpenaren aurretik zegoen. Haien etorrerarekin, behar hori oso ukigarria bihurtu zen. Orain, 2019an, artikulu hau idazten ari naiz 8 nukleoko Intel Core prozesadorea duen ordenagailu eramangarri batean, zeinean ehun prozesu baino gehiago paraleloan exekutatzen ari diren, eta are hari gehiago. Inguruan, telefono apur bat kaxkarra dago, duela urte pare bat erosia, 8 nukleoko prozesadorea du barnean. Baliabide tematikoak artikulu eta bideoz beteta daude, non haien egileek 16 nukleoko prozesadoreak dituzten aurtengo telefono adimendunak miresten dituzten. MS Azure-k 20 nukleoko prozesadore eta 128 TB RAM dituen makina birtual bat eskaintzen du 2 $ orduko baino gutxiagoren truke. Zoritxarrez, ezinezkoa da potentzia maximoa ateratzea eta aprobetxatzea harien interakzioa kudeatu gabe.

terminologia

Prozesua - OS objektua, helbide-espazio isolatua, hariak ditu.
Haria - OS objektu bat, exekuzio unitate txikiena, prozesu baten zatia, hariak memoria eta beste baliabide batzuk partekatzen dituzte prozesu baten barruan.
multiataza - OS jabetza, hainbat prozesu aldi berean exekutatzeko gaitasuna
Nukleo anitzekoa - Prozesadorearen propietate bat, datuak prozesatzeko hainbat nukleo erabiltzeko gaitasuna
Multiprozesaketa - Ordenagailu baten propietate bat, fisikoki hainbat prozesadorerekin aldi berean lan egiteko gaitasuna
Hari anitzekoa — prozesu baten propietate bat, datuen prozesamendua hainbat hariren artean banatzeko gaitasuna.
Paralelismoa - denbora-unitate bakoitzeko hainbat ekintza fisikoki aldi berean egitea
Asinkronia — eragiketa bat gauzatzea prozesamendu hori amaitu arte itxaron gabe; exekuzioaren emaitza geroago prozesatu daiteke.

metafora

Definizio guztiak ez dira onak eta batzuek azalpen osagarria behar dute, beraz, gosaria prestatzeari buruzko metafora bat gehituko diot formalki sartutako terminologiari. Metafora honetan gosaria prestatzea prozesu bat da.

Goizean gosaria prestatzen nuen bitartean (CPU) Sukaldera nator (ordenagailu). 2 esku ditut (koloreak). Sukaldean hainbat gailu daude (IO): labea, bolada, txigorgailua, hozkailua. Gasa piztu, zartagin bat jarri eta olioa botatzen dut berotzen itxaron gabe (modu asinkronikoan, Ez-Blokeatzea-IO-Itxaron), arrautzak hozkailutik atera eta plater batean apurtzen ditut, gero esku batekin irabiatu (1. gaia), eta bigarren (2. gaia) plakari eusten (Baliabide partekatua). Orain katilua piztu nahiko nuke, baina ez daukat esku nahikorik (Haria Gosea) Denbora horretan, zartagina berotzen da (Emaitza prozesatzen) eta harrotu dudana isurtzen dut. Eltzera heldu eta piztu eta ergelki ikusten dut ura bertan irakiten (Blokeatzea-IO-Itxaron), nahiz eta denbora horretan tortilla harrotzen zuen platera garbitu zezakeen.

Tortilla bat prestatu nuen 2 esku bakarrik erabiliz, eta ez daukat gehiago, baina aldi berean, tortilla irabiatzeko momentuan, 3 eragiketa egin ziren aldi berean: tortilla irabiatzea, platera eustea, zartagina berotzea. PUZa ordenagailuaren zatirik azkarrena da, IO gehienetan dena moteltzen dena da, beraz, askotan irtenbide eraginkor bat CPUa zerbaitekin okupatzea da IOtik datuak jasotzen dituen bitartean.

Metaforarekin jarraituz:

  • Tortilla bat prestatzeko prozesuan arropa aldatzen ere saiatuko banintz, multiataza adibide bat izango litzateke. Ñabardura garrantzitsu bat: ordenagailuak askoz hobeak dira horretan pertsonak baino.
  • Hainbat sukaldari dituen sukaldea, adibidez jatetxe batean - nukleo anitzeko ordenagailu bat.
  • Jatetxe asko merkataritza-zentro bateko janari gune batean - datu-zentroan

.NET tresnak

.NET ona da hariekin lan egiten, beste gauza askotan bezala. Bertsio berri bakoitzarekin, gero eta tresna berri gehiago sartzen ditu haiekin lan egiteko, abstrakzio geruza berriak OS harien gainean. Abstrakzioen eraikuntzan lan egiten dutenean, esparru-garatzaileek aukera uzten duten ikuspegi bat erabiltzen dute, goi-mailako abstrakzioa erabiltzean, maila bat edo gehiago beherago jaisteko. Gehienetan hori ez da beharrezkoa, izan ere, eskopeta batekin oinetan tiro egiteko atea irekitzen du, baina batzuetan, kasu bakanetan, egungo abstrakzio mailan konpontzen ez den arazo bat konpontzeko modu bakarra izan daiteke. .

Tresnekin, esparruak eta hirugarrenen paketeek eskaintzen dituzten aplikazioen programazio interfazeak (APIak) esan nahi dut, baita hari anitzeko kodearekin lotutako edozein arazoren bilaketa errazten duten software soluzio osoak ere.

Hari bat hastea

Thread klasea .NET-en klaserik oinarrizkoena da hariekin lan egiteko. Eraikitzaileak bi ordezkari hauetako bat onartzen du:

  • ThreadStart — Ez dago parametrorik
  • ParametrizedThreadStart - motako objektuaren parametro batekin.

Ordezkaria sortu berri den harian exekutatuko da Start metodoari deitu ondoren. ParametrizedThreadStart motako delegatu bat eraikitzaileari pasatu bazaio, objektu bat Start metodora pasatu behar da. Mekanismo hau beharrezkoa da edozein tokiko informazioa korrontera transferitzeko. Aipatzekoa da haria sortzea eragiketa garestia dela eta haria bera objektu astuna dela, gutxienez 1MB memoria pilatzen duelako eta OS APIarekin elkarreragina behar duelako.

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

ThreadPool klaseak igerileku kontzeptua adierazten du. .NET-en, hari multzoa ingeniaritza zati bat da, eta Microsoft-eko garatzaileek ahalegin handia egin dute hainbat eszenatokitan modu egokian funtzionatzen duela ziurtatzeko.

Kontzeptu orokorra:

Aplikazioa abiarazten den unetik, hainbat hari erreserbatan sortzen ditu atzealdean eta horiek erabiltzeko aukera ematen du. Hariak maiz eta kopuru handitan erabiltzen badira, igerilekua zabaltzen da deitzailearen beharrei erantzuteko. Igerilekuan une egokian hari librerik ez dagoenean, harietako bat itzultzeko itxarongo da, edo beste bat sortuko du. Horren ondorioz, hari multzoa epe laburreko ekintza batzuetarako oso egokia da eta aplikazioaren funtzionamendu osoan zerbitzu gisa exekutatzen diren eragiketetarako ez da egokia.

Pool-eko hari bat erabiltzeko, WaitCallback motako delegatu bat onartzen duen QueueUserWorkItem metodo bat dago, ParametrizedThreadStart-en sinadura bera duena, eta hari pasatzen zaion parametroak funtzio bera betetzen du.

ThreadPool.QueueUserWorkItem(...);

Ezagutzen ez den hari multzoko RegisterWaitForSingleObject metodoa blokeatzen ez diren IO eragiketak antolatzeko erabiltzen da. Metodo horretara pasatu den ordezkariari deituko zaio metodora pasatzen den WaitHandle "Askatua" denean.

ThreadPool.RegisterWaitForSingleObject(...)

.NET-ek hari-tenporizadore bat du eta WinForms/WPF-ren tenporizadoreetatik desberdina da bere kudeatzailea igerilekutik hartutako hari batean deituko baita.

System.Threading.Timer

Ordezkari bat igerilekuko hari batera exekutatzeko bidaltzeko modu exotiko samarra ere badago: BeginInvoke metodoa.

DelegateInstance.BeginInvoke

Goiko metodo asko dei daitezkeen funtzioari buruz laburki luzatu nahi nuke - Kernel32.dll Win32 APItik CreateThread. Bada modu bat, extern metodoen mekanismoari esker, funtzio honi deitzeko. Behin bakarrik ikusi dut halako dei bat ondare-kodearen adibide ikaragarri batean, eta hori egin zuen egilearen motibazioa oraindik misterio bat izaten jarraitzen du niretzat.

Kernel32.dll CreateThread

Hariak ikusi eta araztea

Zuk sortutako hariak, hirugarrenen osagai guztiak eta .NET igerilekua Visual Studio-ko Threads leihoan ikus daitezke. Leiho honek hariaren informazioa soilik bistaratuko du aplikazioa arazketan eta Break moduan dagoenean. Hemen hari bakoitzaren pila-izenak eta lehentasunak eroso ikus ditzakezu eta arazketa hari zehatz batera alda dezakezu. Thread klasearen Priority propietatea erabiliz, hari baten lehentasuna ezar dezakezu, OC eta CLR-k gomendio gisa hautemango duten prozesadorearen denbora harien artean banatzean.

.NET: Multithreading eta asinkronia lantzeko tresnak. 1. zatia

Zeregin Liburutegi Paraleloa

Task Parallel Library (TPL) .NET 4.0-n sartu zen. Orain estandarra eta tresna nagusia da asinkronia lantzeko. Ikuspegi zaharrago bat erabiltzen duen edozein kodea ondaretzat hartzen da. TPLren oinarrizko unitatea System.Threading.Tasks izen-eremuko Task klasea da. Zeregin bat hari baten gaineko abstrakzio bat da. C# hizkuntzaren bertsio berriarekin, Zereginekin lan egiteko modu dotore bat lortu dugu - async/wait operadoreak. Kontzeptu hauek kode asinkronoa sinple eta sinkronoa balitz bezala idaztea ahalbidetu zuten, honek harien barne-funtzionamendua gutxi ezagutzen zuten pertsonek ere haiek erabiltzen dituzten aplikazioak idaztea posible egin zuen, eragiketa luzeak egitean izozten ez diren aplikazioak. Async/wait erabiltzea artikulu baten edo are gehiagoren gaia da, baina horren mamia esaldi batzuetan jasotzen saiatuko naiz:

  • async Task edo void itzultzen duen metodo baten aldatzailea da
  • eta itxaron Task itxaroten blokeatzen ez duen operadorea da.

Berriro ere: await operadoreak, kasu orokorrean (salbuespenak daude), uneko exekuzioaren haria gehiago kaleratuko du, eta Zereginak exekuzioa amaitzen duenean, eta haria (hain zuzen ere, zuzenagoa izango litzateke testuingurua esatea). , baina gehiago geroago) metodoa gehiago exekutatzen jarraituko du. .NET-en barnean, mekanismo hau yield return bezala inplementatzen da, idatzizko metodoa klase oso batean bihurtzen denean, hau da, egoera-makina bat da eta egoera horien arabera zati banatan exekutatu daiteke. Interesa duen edonork asynс/wait erabiliz edozein kode sinple idatz dezake, konpilatu eta ikusi muntaia JetBrains dotPeek erabiliz Compiler Generated Code gaituta.

Ikus ditzagun Zeregin bat abiarazteko eta erabiltzeko aukerak. Beheko kode adibidean, ezer erabilgarria ez duen zeregin berri bat sortzen dugu (Thread.Sleep (10000)), baina bizitza errealean hau PUZaren lan intentsiboa izan beharko litzateke.

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
}

Zeregin bat aukera ugarirekin sortzen da:

  • LongRunning-ek zeregina azkar beteko ez den iradokizuna da, eta horrek esan nahi du igerilekutik haririk ez hartzea kontuan hartzea, baina Zeregin honetarako bereizi bat sortzea besteei kalterik ez egiteko.
  • AttachedToParent - Zereginak hierarkia batean antola daitezke. Aukera hau erabili bazen, Zereginak berak amaitu duen egoeran egon daiteke eta bere seme-alaben exekuzioaren zain dagoen.
  • PreferFairness - esan nahi du hobe litzatekeela exekutatzeko bidalitako Zereginak lehenago exekutatu ondoren bidalitakoak baino lehen. Baina hau gomendio bat besterik ez da eta emaitzak ez daude bermatuta.

Metodoari pasatzen zaion bigarren parametroa CancellationToken da. Eragiketa bat hasi ondoren baliogabetzea behar bezala kudeatzeko, exekutatzen ari den kodea CancellationToken egoeraren egiaztapenekin bete behar da. Egiaztapenik ez badago, CancellationTokenSource objektuan deitutako Cancel metodoak Zereginaren exekuzioa hasi baino lehen gelditu ahal izango du.

Azken parametroa TaskScheduler motako objektu planifikatzailea da. Klase hau eta bere ondorengoak Zereginak harien artean banatzeko estrategiak kudeatzeko diseinatuta daude; lehenespenez, Zereginak igerilekuko ausazko hari batean exekutatuko dira.

Await operadorea sortutako Zereginari aplikatzen zaio, hau da, horren ondoren idatzitako kodea, halakorik badago, itxaron aurreko kodearen testuinguru berean (askotan horrek hari berean esan nahi du) exekutatu egingo da.

Metodoa async void gisa markatuta dago, hau da, await operadorea erabil dezake, baina deitzeko kodeak ezin izango du exekutatzeko itxaron. Ezaugarri hori beharrezkoa bada, metodoak Task itzuli behar du. Async void markatutako metodoak nahiko ohikoak dira: oro har, gertaeren kudeatzaileak edo suaren eta ahaztuaren printzipioan lan egiten duten beste metodo batzuk dira. Exekuzioaren amaierara arte itxaron ez ezik, emaitza ere itzuli behar baduzu, Task erabili behar duzu.

StartNew metodoak itzuli duen Zereginean, baita beste edozeinetan ere, ConfigureAwait metodoari dei diezaiokezu parametro faltsuarekin, gero itxaron ondoren exekuzioak jarraituko du ez harrapatutako testuinguruan, baizik eta arbitrario batean. Hau beti egin behar da exekuzio testuingurua itxaron ondoren kodearentzat garrantzitsua ez denean. Hau MS-ren gomendioa da liburutegi batean paketatuta entregatuko den kodea idaztean.

Goazen pixka bat gehiago Zeregin bat amaitu arte itxaron dezakezun. Jarraian, kode-adibide bat dago, itxaropena baldintza ondo egiten denean eta baldintza gaizki egiten denean iruzkinekin.

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
}

Lehenengo adibidean, Zereginak deitzen duen haria blokeatu gabe amaitu arte itxarongo dugu; emaitza dagoeneko bertan dagoenean bakarrik itzuliko gara prozesatzen; ordura arte, deitzen duen haria bere esku geratzen da.

Bigarren aukeran, deitzen duen haria blokeatzen dugu metodoaren emaitza kalkulatu arte. Hau txarra da, ez bakarrik hari bat, programaren baliabide baliotsu bat, alferkeria sinplez okupatu dugulako, baizik eta deitzen dugun metodoaren kodea dauka zain dagoelako eta sinkronizazio testuinguruak deitzen duen harira itzultzea eskatzen duelako. itxaron, orduan blokeo bat lortuko dugu : deitzen duen haria metodo asinkronoaren emaitza kalkulatzeko zain dago, metodo asinkronoa alferrik saiatzen da deitzen duen harian exekutatzen jarraitzen.

Ikuspegi honen beste desabantaila bat erroreen kudeaketa konplikatua da. Kontua da kode asinkronoan akatsak async/wait erabiltzean oso erraz maneiatzen direla - kodea sinkronoa balitz bezala jokatzen dute. Zeregin bati itxaron exorzismo sinkronikoa aplikatzen badiogu, jatorrizko salbuespena AggregateException bihurtzen da, hau da. Salbuespena kudeatzeko, InnerException mota aztertu eta if kate bat idatzi beharko duzu catch bloke baten barruan edo catch erabili eraikitzean, C# munduan ezagunagoa den catch blokeen katearen ordez.

Hirugarren eta azken adibideak ere txarto markatuta daude arrazoi beragatik eta arazo berdinak dituzte.

WhenAny eta WhenAll metodoak oso erosoak dira Zeregin talde baten zain egoteko; Zeregin talde bat bakarrean biltzen dute, taldeko Zeregin bat lehen aldiz abiarazten denean edo denek exekuzioa amaitu dutenean abiaraziko dena.

Hariak gelditzea

Hainbat arrazoirengatik, fluxua etetea beharrezkoa izan daiteke hasi ondoren. Horretarako hainbat modu daude. Thread klaseak behar bezala izendatutako bi metodo ditu: Abortu и Eten. Lehenengoa ez da oso gomendagarria erabiltzeko, izan ere ausazko edozein momentutan deitu ondoren, edozein instrukzio prozesatzen ari den bitartean, salbuespen bat botako da ThreadAbortedException. Ez duzu espero halako salbuespenik botako denik edozein aldagai oso gehitzean, ezta? Eta metodo hau erabiltzean, hau oso egoera erreala da. CLR-k kode-atal jakin batean salbuespen bat sortzea eragotzi behar baduzu, deietan bildu dezakezu Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Finally bloke batean idatzitako edozein kodea honelako deietan biltzen da. Hori dela eta, marko-kodearen sakontasunean entsegu hutsa duten blokeak aurki ditzakezu, baina azkenean hutsik ez. Microsoft-ek metodo hau hainbeste gomendatzen du, ez baitute .net core-n sartu.

Eten metodoak hobeto funtzionatzen du. Haria eten dezake salbuespen batekin ThreadInterruptedException haria itxarote egoeran dagoen une horietan bakarrik. Egoera honetan sartzen da zintzilik dagoen bitartean WaitHandle, blokeoa edo Thread.Sleep deitu ondoren.

Goian azaldutako bi aukera txarrak dira ezustekoak direla eta. Irtenbidea egitura bat erabiltzea da CancellationToken eta klasea CancellationTokenSource. Kontua hau da: CancellationTokenSource klasearen instantzia bat sortzen da eta bere jabe denak bakarrik geldi dezake eragiketa metodoari deituz Utzi. CancellationToken bakarrik pasatzen da eragiketari berari. CancellationToken jabeek ezin dute eragiketa bertan behera utzi, baina eragiketa bertan behera den ala ez egiaztatu baino ezin dute egin. Honetarako propietate boolearra dago IsCancellationRequested eta metodoa ThrowIfCancelRequested. Azken honek salbuespen bat botako du TaskCancelledException Cancellation Metodoa parrotatzen ari den CancellationToken instantzian deitu bada. Eta hau da erabiltzea gomendatzen dudan metodoa. Hau aurreko aukeren hobekuntza da, salbuespen eragiketa bat zein puntutan bertan behera utz daitekeen kontrol osoa lortuz.

Hari bat geldiarazteko aukerarik basatiena Win32 API TerminateThread funtzioari deitzea da. Funtzio honi deitu ondoren CLRren portaera ezustekoa izan daiteke. MSDNn honako hau idazten da funtzio honi buruz: “TerminateThread funtzio arriskutsu bat da, kasu larrienetan bakarrik erabili behar dena. “

Oinarritutako APIa Zereginetan oinarritutako bihurtzea FromAsync metodoa erabiliz

Zereginak aurkeztu eta garatzaile gehienentzat izu lasaia sortzeari utzi ondoren hasitako proiektu batean lan egiteko zortea baduzu, orduan ez duzu API zahar askori aurre egin beharko, bai hirugarrenenei, bai zure taldekoei. torturatu du iraganean. Zorionez, .NET Framework taldeak zaindu gintuen, nahiz eta beharbada helburua geure burua zaintzea izan. Dena den, .NET-ek hainbat tresna ditu programazio asinkronoko ikuspegi zaharretan idatzitako kodea berrira bihurtzeko. Horietako bat TaskFactory-ren FromAsync metodoa da. Beheko kodearen adibidean, WebRequest klaseko metodo asinkroniko zaharrak Zeregin batean biltzen ditut metodo hau erabiliz.

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

Hau adibide bat besterik ez da eta nekez egin beharko duzu hori barne-motekin, baina edozein proiektu zahar hori jasotzen duten IAsyncResult eta EndDoSomething metodoak itzultzen dituzten BeginDoSomething metodoez beteta dago.

Bihurtu legatutako APIa Zereginen Oinarritutakoa TaskCompletionSource klasea erabiliz

Kontuan hartu beharreko beste tresna garrantzitsu bat klasea da TaskCompletionSource. Funtzioei, helburuari eta funtzionamendu-printzipioari dagokionez, ThreadPool klaseko RegisterWaitForSingleObject metodoa gogora ekar dezake, goian idatzi dudana. Klase hau erabiliz, API asinkrono zaharrak erraz eta eroso bil ditzakezu Zereginetan.

TaskFactory klaseko FromAsync metodoari buruz hitz egin dudala esango duzu helburu horietarako. Hemen Microsoft-ek azken 15 urteotan eskaini duen .net-en eredu asinkronoen garapenaren historia osoa gogoratu beharko dugu: Task-Based Asynchronous Pattern (TAP) aurretik, Programazio Asynchronous Pattern (APP) zegoen. metodoei buruzkoa zen HasiEgin Zerbait itzultzen IAsyncResult eta metodoak amaieraOnartzen duen zerbait egin eta urte hauetako ondarerako FromAsync metodoa perfektua da, baina denborarekin, Gertaeren Oinarritutako Eredu Asinkronoarekin ordezkatu zen (EAP), eragiketa asinkronoa amaitzean gertaera bat sortuko zela suposatzen zuen.

TaskCompletionSource ezin hobea da Gertaeren ereduaren inguruan eraikitako Zereginak eta API zaharrak biltzeko. Bere lanaren funtsa honako hau da: klase honetako objektu batek Task motako propietate publiko bat du, zeinaren egoera TaskCompletionSource klaseko SetResult, SetException, etab. metodoen bidez kontrolatu daitekeena. Await operadorea Zeregin honi aplikatu zaion tokietan, exekutatu edo huts egingo da, TaskCompletionSource-ri aplikatutako metodoaren arabera salbuespen batekin. Oraindik argi ez badago, ikus dezagun kode-adibide hau, non EAP API zahar batzuk TaskCompletionSource erabiliz TaskCompletionSource bat erabiliz: Zereginak amaitutako egoerara pasatuko dira eta itxaron operadorea aplikatu duen metodoa. Zeregin honi bere exekuzioari ekingo dio objektua jaso ondoren ondorioz.

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 Aholkuak eta trikimailuak

API zaharrak biltzea ez da TaskCompletionSource erabiliz egin daitekeen guztia. Klase hau erabiltzeak aukera interesgarri bat irekitzen du hariek okupatzen ez dituzten atazetan hainbat API diseinatzeko. Eta korrontea, gogoratzen dugunez, baliabide garestia da eta haien kopurua mugatua da (batez ere RAM kopuruagatik). Muga hori erraz lor daiteke, adibidez, negozio-logika konplexua duen kargatutako web aplikazio bat garatuz. Azter ditzagun Long-Polling bezalako trikimailu bat ezartzerakoan hitz egiten ari naizen aukerak.

Laburbilduz, trikimailuaren funtsa hau da: APItik informazioa jaso behar duzu bere alboan gertatzen diren gertakari batzuei buruz, eta APIak, arrazoiren bategatik, ezin du gertaera jakinarazi, baina egoera itzul dezake soilik. Horien adibide dira WebSocket-en garaiak baino lehen HTTPren gainean eraikitako API guztiak edo arrazoiren batengatik teknologia hau erabiltzea ezinezkoa zenean. Bezeroak HTTP zerbitzariari galdetu diezaioke. HTTP zerbitzariak berak ezin du bezeroarekin komunikazioa hasi. Irtenbide sinple bat zerbitzaria tenporizadorea erabiliz galdetzea da, baina honek zerbitzarian karga gehigarria eta atzerapen gehigarria sortzen du batez beste TimerInterval / 2. Horri aurre egiteko, Long Polling izeneko trikimailu bat asmatu zen, eta erantzuna atzeratzea dakar. zerbitzaria Denbora-muga iraungi edo gertaera bat gertatu arte. Gertaera gertatu bada, prozesatzen da, ez bada, eskaera berriro bidaltzen da.

while(!eventOccures && !timeoutExceeded)  {

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

Baina horrelako irtenbidea izugarria izango da ekitaldiaren zain dauden bezero kopurua handitu bezain pronto, zeren... Horrelako bezero bakoitzak hari oso bat hartzen du gertaera baten zain. Bai, eta gertaera abiarazten denean 1 ms atzerapen gehigarria lortzen dugu, gehienetan hori ez da esanguratsua, baina zergatik egin softwarea izan daitekeena baino okerragoa? Thread.Sleep(1) kentzen badugu, orduan alferrik kargatuko dugu prozesadore-nukleo bat %100 inaktibo, alferrikako ziklo batean biratzen. TaskCompletionSource erabiliz kode hau erraz birsor dezakezu eta goian identifikatutako arazo guztiak konpon ditzakezu:

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

Kode hau ez dago produkziorako prest, demo bat besterik ez dago. Kasu errealetan erabiltzeko, gutxienez, mezu bat inor espero ez den momentuan iristen den egoera kudeatu behar duzu: kasu honetan, AsseptMessageAsync metodoak dagoeneko amaitutako Zeregin bat itzuli beharko luke. Kasurik ohikoena bada, ValueTask erabiltzea pentsa dezakezu.

Mezu baten eskaera jasotzen dugunean, TaskCompletionSource bat sortu eta jartzen dugu hiztegian, eta gero itxaron zer gertatzen den lehenik: zehaztutako denbora tartea iraungi edo mezu bat jasotzen da.

ValueTask: zergatik eta nola

Async/wait operadoreek, yield return operadorea bezala, egoera makina bat sortzen dute metodotik, eta hau objektu berri bat sortzea da, ia beti ez da garrantzitsua, baina kasu bakanetan arazo bat sor dezake. Kasu hau benetan sarri deitzen den metodoa izan daiteke, segundoko hamarnaka eta ehunka mila deiez ari gara. Metodo bat kasu gehienetan itxaron metodo guztiak saihestuz emaitza bat itzultzen duen moduan idazten bada, orduan .NET-ek hau optimizatzeko tresna bat eskaintzen du: ValueTask egitura. Argi uzteko, ikus dezagun bere erabileraren adibide bat: bada oso maiz joaten garen cache bat. Balore batzuk daude bertan eta, besterik gabe, itzultzen ditugu; hala ez bada, IO geldo batzuetara joaten gara horiek lortzeko. Azken hau modu asinkronoan egin nahi dut, hau da, metodo osoa asinkronoa bihurtzen da. Beraz, metodoa idazteko modu argia honako hau da:

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

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

Apur bat optimizatzeko nahia eta Roslyn-ek kode hau konpilatzean sortuko duenaren beldur apur bat dela eta, adibide hau honela berridatzi dezakezu:

public Task<string> GetById(int id) {

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

Izan ere, kasu honetan irtenbiderik onena bide beroa optimizatzea izango litzateke, hots, hiztegitik balio bat lortzea alferrikako esleipenik eta GC-n kargarik gabe, eta datuetarako IO-ra joan behar dugun kasu bakanetan, berriz. , dena plus / ken modu zaharrean geratuko da:

public ValueTask<string> GetById(int id) {

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

Ikus dezagun hurbilagotik kode zati hau: cachean balio bat badago, egitura bat sortzen dugu, bestela benetako zeregina esanguratsu batean bilduko da. Deitzen duen kodeak ez du axola kode hau zein bidetan exekutatu den: ValueTask, C# sintaxiaren ikuspuntutik, Task arrunt baten portaera berdina izango du kasu honetan.

TaskSchedulers: zereginak abiarazteko estrategiak kudeatzea

Kontuan hartu nahiko nukeen hurrengo APIa klasea da zeregin-antolatzaile eta bere eratorriak. Lehen aipatu dut TPL-k gaitasuna duela Zereginak harietan banatzeko estrategiak kudeatzeko. Horrelako estrategiak TaskScheduler klasearen ondorengoetan definitzen dira. Behar duzun ia edozein estrategia aurki dezakezu liburutegian. Luzapen ParaleloakExtras, Microsoft-ek garatua, baina ez .NET-en parte, baina Nuget pakete gisa hornitua. Ikus ditzagun labur-labur horietako batzuk:

  • Current ThreadTaskScheduler — Zereginak uneko harian exekutatzen ditu
  • LimitedConcurrencyLevelTaskScheduler — Eraikitzailean onartzen den N parametroak aldi berean exekutatutako Zereginen kopurua mugatzen du
  • AgindutakoTaskScheduler — LimitedConcurrencyLevelTaskScheduler(1) gisa definitzen da, beraz, zereginak sekuentzialki exekutatuko dira.
  • WorkStealingTaskScheduler - tresnak lan-lapurreta zereginen banaketaren ikuspegia. Funtsean ThreadPool bereizi bat da. .NET ThreadPool-en klase estatikoa den arazoa konpontzen du, aplikazio guztietarako bat, eta horrek esan nahi du programaren zati batean gainkargatzeak edo oker erabiltzeak bigarren mailako efektuak ekar ditzakeela beste batean. Gainera, oso zaila da akats horien zergatia ulertzea. Hori. Baliteke ThreadPool-en erabilera oldarkorra eta ezustekoa izan daitekeen programaren zatietan WorkStealingTaskSchedulers erabili beharra izatea.
  • QueuedTaskScheduler — lehentasunezko ilararen arauen arabera zereginak egiteko aukera ematen du
  • ThreadPerTaskScheduler — Hari bereizi bat sortzen du bertan exekutatzen den Zeregin bakoitzeko. Ezusteko luzea behar duten zereginetarako erabilgarria izan daiteke.

Xehetasun ona dago artikuluan TaskSchedulers-i buruz Microsoft blogean.

Atazekin erlazionatutako guztia arazketa erosoa izateko, Visual Studio-k Zereginen leiho bat du. Leiho honetan zereginaren uneko egoera ikus dezakezu eta unean exekutatzen ari den kode lerrora salto egin dezakezu.

.NET: Multithreading eta asinkronia lantzeko tresnak. 1. zatia

PLinq eta Paralelo klasea

Zereginez eta haiei buruz esandako guztiaz gain, beste bi tresna interesgarri daude .NETen: PLinq (Linq2Parallel) eta Parallel klasea. Lehenengoak Linq eragiketa guztien exekuzio paraleloa agintzen du hainbat haritan. Hari kopurua WithDegreeOfParallelism luzapen metodoa erabiliz konfigura daiteke. Zoritxarrez, gehienetan PLinq-ek bere modu lehenetsian ez du informazio nahikorik zure datu-iturriaren barnekoei buruz abiadura irabazi nabarmena emateko, bestetik, probatzearen kostua oso baxua da: aurretik AsParallel metodoa deitu behar duzu. Linq metodoen katea eta errendimendu probak exekutatu. Gainera, PLinq-i informazio gehigarria pasa daiteke zure datu-iturburuaren izaerari buruz Partizioak mekanismoa erabiliz. Gehiago irakur dezakezu Hemen и Hemen.

Paralel klase estatikoak Foreach bilduma batean paraleloan errepikatzeko metodoak eskaintzen ditu, For begizta bat exekutatzeko eta Invoke paraleloan hainbat delegatu exekutatzeko. Uneko hariaren exekuzioa geldituko da kalkuluak amaitu arte. Hari kopurua konfigura daiteke ParallelOptions azken argumentu gisa pasatuz. TaskScheduler eta CancellationToken ere zehaztu ditzakezu aukerak erabiliz.

Findings

Artikulu hau idazten hasi nintzenean, nire txostenaren materialetan eta ondoren egindako lanetan bildutako informazioan oinarrituta, ez nuen espero hainbeste egongo zenik. Orain, artikulu hau idazten ari naizen testu-editoreak 15. orrialdea joan dela esaten didanean, behin-behineko emaitzak laburbilduko ditut. Beste trikimailu, API, tresna bisualak eta hutsuneak hurrengo artikuluan azalduko dira.

Ondorioak:

  • Hari, asinkronia eta paralelismoa lantzeko tresnak ezagutu behar dituzu ordenagailu modernoen baliabideak erabiltzeko.
  • .NET-ek hainbat tresna ditu helburu horietarako
  • Guztiak ez ziren aldi berean agertu, beraz, maiz aurkitu ditzakezu ondarea, hala ere, API zaharrak esfortzu handirik gabe bihurtzeko moduak daude.
  • .NET-en hariekin lan egitea Thread eta ThreadPool klaseek adierazten dute
  • Thread.Abort, Thread.Interrupt eta Win32 API TerminateThread metodoak arriskutsuak dira eta ez dira erabiltzea gomendatzen. Horren ordez, hobe da CancellationToken mekanismoa erabiltzea
  • Fluxua baliabide baliotsua da eta bere hornidura mugatua da. Ekitaldien zain hariak lanpetuta dauden egoerak saihestu behar dira. Horretarako erosoa da TaskCompletionSource klasea erabiltzea
  • Paralelismoa eta asinkronia lantzeko .NET tresna indartsuenak eta aurreratuenak Zereginak dira.
  • C# async/wait operadoreek blokeatzen ez duten itxaronaren kontzeptua ezartzen dute
  • Zereginen banaketa kontrola dezakezu harietan zehar TaskScheduler-en eratorritako klaseak erabiliz
  • ValueTask egitura erabilgarria izan daiteke hot-paths eta memoria-trafikoa optimizatzeko
  • Visual Studio-ren Tasks eta Threads leihoek informazio asko eskaintzen dute hari anitzeko edo asinkronoa den kode arazteko.
  • PLinq tresna polita da, baina baliteke zure datu-iturburuari buruzko informazio nahikorik ez izatea, baina partizio-mekanismoa erabiliz konpondu daiteke.
  • Jarraitu ahal izateko ...

Iturria: www.habr.com

Gehitu iruzkin berria