.NET: Iloj por labori kun multfadenado kaj malsinkronio. Parto 1

Mi publikigas la originalan artikolon pri Habr, kies traduko estas afiŝita en la korporacia blogo.

La bezono fari ion nesinkrone, sen atendi la rezulton ĉi tie kaj nun, aŭ dividi grandan laboron inter pluraj unuoj plenumantaj ĝin, ekzistis antaŭ la apero de komputiloj. Kun ilia alveno, ĉi tiu bezono fariĝis tre palpebla. Nun, en 2019, mi tajpas ĉi tiun artikolon sur tekkomputilo kun 8-kerna procesoro Intel Core, sur kiu pli ol cent procezoj funkcias paralele, kaj eĉ pli da fadenoj. Proksime, estas iomete mizera telefono, aĉetita antaŭ kelkaj jaroj, ĝi havas 8-kernan procesoron surŝipe. Temaj rimedoj estas plenaj de artikoloj kaj filmetoj, kie iliaj aŭtoroj admiras ĉi-jarajn ĉefpoŝtelefonojn, kiuj havas 16-kernajn procesorojn. MS Azure provizas virtualan maŝinon kun 20 kernprocesoro kaj 128 TB RAM por malpli ol $ 2/horo. Bedaŭrinde, estas neeble ĉerpi la maksimumon kaj utiligi ĉi tiun potencon sen povi administri la interagadon de fadenoj.

Terminologio

Procezo - OS objekto, izolita adresspaco, enhavas fadenojn.
Fadeno - OS objekto, la plej malgranda unuo de ekzekuto, parto de procezo, fadenoj kunhavas memoron kaj aliajn rimedojn inter si ene de procezo.
Multitarea - OS-posedaĵo, la kapablo ruli plurajn procezojn samtempe
Plurkerno — eco de la procesoro, la kapablo uzi plurajn kernojn por datumtraktado
Multiprocesado - eco de komputilo, la kapablo samtempe labori kun pluraj procesoroj fizike
Multfadenado — propraĵo de procezo, la kapablo distribui datumtraktadon inter pluraj fadenoj.
Paralelismo - plenumi plurajn agojn fizike samtempe po unuo de tempo
Nesinkronio — plenumo de operacio sen atendado de la fino de ĉi tiu prilaborado; la rezulto de la ekzekuto povas esti procesita poste.

Metaforo

Ne ĉiuj difinoj estas bonaj kaj kelkaj bezonas plian klarigon, do mi aldonos metaforon pri kuirado de matenmanĝo al la formale enkondukita terminologio. Kuiri matenmanĝon en ĉi tiu metaforo estas procezo.

Preparante matenmanĝon mi (CPU) Mi venas al la kuirejo (Komputilo). Mi havas 2 manojn (Koloroj). Estas kelkaj aparatoj en la kuirejo (IO): forno, kaldrono, panrostilo, fridujo. Mi enŝaltas la gason, metas paton sur ĝin kaj verŝas oleon en ĝin sen atendi ke ĝi varmiĝos (nesinkrone, Non-Blocking-IO-Wait), mi elprenas la ovojn el la fridujo kaj rompas ilin en teleron, poste batas ilin per unu mano (Fadeno #1), kaj dua (Fadeno #2) tenante la teleron (Komuna Rimedo). Nun mi ŝatus ŝalti la kaldrono, sed mi ne havas sufiĉe da manoj (Fadeno Malsato) Dum ĉi tiu tempo, la pato varmiĝas (Pretigante la rezulton) en kiun mi verŝas tion, kion mi vipis. Mi atingas la kaldrono kaj enŝaltas ĝin kaj stulte rigardas la akvon boli en ĝi (Blokado-IO-Atendu), kvankam dum tiu ĉi tempo li povus esti lavinta la teleron kie li vipis la omleton.

Mi kuiris omleton uzante nur 2 manojn, kaj mi ne havas pli, sed samtempe, en la momento de vipi la omleton, okazis samtempe 3 operacioj: vipi la omleton, tenante la teleron, varmigi la paton. La CPU estas la plej rapida parto de la komputilo, IO estas kio plej ofte ĉio malrapidiĝas, do ofte efika solvo estas okupi la CPU per io dum ricevado de datumoj de IO.

Daŭrigante la metaforon:

  • Se en la procezo de preparado de omleto, mi ankaŭ provus ŝanĝi vestaĵojn, ĉi tio estus ekzemplo de multfarado. Grava nuanco: komputiloj estas multe pli bonaj pri tio ol homoj.
  • Kuirejo kun pluraj kuiristoj, ekzemple en restoracio - plurkerna komputilo.
  • Multaj restoracioj en manĝejo en butikcentro - datumcentro

.NET Iloj

.NET kapablas labori kun fadenoj, kiel kun multaj aliaj aferoj. Kun ĉiu nova versio, ĝi enkondukas pli kaj pli novajn ilojn por labori kun ili, novajn tavolojn de abstraktado super OS-fadenoj. Laborante kun la konstruado de abstraktaĵoj, kadroprogramistoj uzas aliron kiu lasas la ŝancon, dum uzado de altnivela abstraktado, malsupreniri unu aŭ pluraj nivelojn malsupre. Plej ofte ĉi tio ne estas necesa, fakte ĝi malfermas la pordon por pafi vin en la piedon per ĉaspafilo, sed foje, en maloftaj kazoj, ĝi povas esti la sola maniero solvi problemon, kiu ne estas solvita ĉe la nuna nivelo de abstraktado. .

Per iloj, mi celas ambaŭ aplikajn programajn interfacojn (API) provizitajn de la kadro kaj triaj pakaĵoj, kaj ankaŭ tutajn programajn solvojn, kiuj simpligas la serĉon de ajnaj problemoj rilataj al multfadena kodo.

Komencante fadenon

La Fadena klaso estas la plej baza klaso en .NET por labori kun fadenoj. La konstrukciisto akceptas unu el du delegitoj:

  • ThreadStart — Neniuj parametroj
  • ParametrizedThreadStart - kun unu parametro de tipobjekto.

La delegito estos ekzekutita en la nove kreita fadeno post vokado de la Komenca metodo.Se delegito de tipo ParametrizedThreadStart estis transdonita al la konstrukciisto, tiam objekto devas esti pasita al la Komenca metodo. Ĉi tiu mekanismo estas necesa por transdoni ajnan lokan informon al la rivereto. Indas noti, ke krei fadenon estas multekosta operacio, kaj la fadeno mem estas peza objekto, almenaŭ ĉar ĝi asignas 1MB da memoro sur la stako kaj postulas interagadon kun la OS-API.

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

La ThreadPool klaso reprezentas la koncepton de naĝejo. En .NET, la fadena naĝejo estas peco de inĝenieristiko, kaj la programistoj ĉe Mikrosofto multe klopodis por certigi, ke ĝi funkcias optimume en diversaj scenaroj.

Ĝenerala koncepto:

De la momento, kiam la aplikaĵo komenciĝas, ĝi kreas plurajn fadenojn en rezervo en la fono kaj disponigas la kapablon preni ilin por uzo. Se fadenoj estas uzitaj ofte kaj en grandaj nombroj, la naĝejo disetendiĝas por renkonti la bezonojn de la alvokanto. Kiam ne estas liberaj fadenoj en la naĝejo en la ĝusta tempo, ĝi aŭ atendos ke unu el la fadenoj revenos, aŭ kreos novan. Sekvas, ke la fadena naĝejo estas bonega por iuj mallongperspektivaj agoj kaj nebone taŭgas por operacioj kiuj funkcias kiel servoj dum la tuta operacio de la aplikaĵo.

Por uzi fadenon el la naĝejo, ekzistas metodo QueueUserWorkItem, kiu akceptas delegiton de tipo WaitCallback, kiu havas la saman subskribon kiel ParametrizedThreadStart, kaj la parametro pasita al ĝi plenumas la saman funkcion.

ThreadPool.QueueUserWorkItem(...);

La malpli konata fadena pool-metodo RegisterWaitForSingleObject estas uzata por organizi ne-blokajn IO-operaciojn. La delegito pasita al ĉi tiu metodo estos vokita kiam la WaitHandle pasita al la metodo estas "Eldonita".

ThreadPool.RegisterWaitForSingleObject(...)

.NET havas fadenan tempigilon kaj ĝi diferencas de WinForms/WPF-tempigiloj en tio, ke ĝia prizorganto estos vokita sur fadeno prenita el la naĝejo.

System.Threading.Timer

Ankaŭ ekzistas sufiĉe ekzotika maniero sendi delegiton por ekzekuto al fadeno de la naĝejo - la metodo BeginInvoke.

DelegateInstance.BeginInvoke

Mi ŝatus mallonge resti pri la funkcio al kiu multaj el la supraj metodoj povas esti nomataj - CreateThread de Kernel32.dll Win32 API. Estas maniero, danke al la mekanismo de eksteraj metodoj, nomi ĉi tiun funkcion. Mi vidis tian vokon nur unufoje en terura ekzemplo de hereda kodo, kaj la instigo de la aŭtoro, kiu faris ĝuste tion, ankoraŭ restas por mi mistero.

Kernel32.dll CreateThread

Vidado kaj Sencimigado de Fadenoj

Fadenoj kreitaj de vi, ĉiuj triaj komponantoj kaj la .NET-poolo videblas en la fenestro de Fadenoj de Visual Studio. Ĉi tiu fenestro nur montros fadenajn informojn kiam la aplikaĵo estas sub sencimigo kaj en Break-reĝimo. Ĉi tie vi povas oportune vidi la staknomojn kaj prioritatojn de ĉiu fadeno, kaj ŝanĝi sencimigon al specifa fadeno. Uzante la posedaĵon Priority de la Thread-klaso, vi povas agordi la prioritaton de fadeno, kiun la OC kaj CLR perceptos kiel rekomendon kiam dividas procesoran tempon inter fadenoj.

.NET: Iloj por labori kun multfadenado kaj malsinkronio. Parto 1

Tasko Paralela Biblioteko

Task Parallel Library (TPL) estis lanĉita en .NET 4.0. Nun ĝi estas la normo kaj la ĉefa ilo por labori kun malsinkronio. Ajna kodo kiu uzas pli malnovan aliron estas konsiderita heredaĵo. La baza unuo de TPL estas la Task-klaso de la System.Threading.Tasks nomspaco. Tasko estas abstraktaĵo super fadeno. Kun la nova versio de la lingvo C#, ni ricevis elegantan manieron labori kun Taskoj - async/wait operators. Ĉi tiuj konceptoj ebligis skribi nesinkronan kodon kvazaŭ ĝi estus simpla kaj sinkrona, tio ebligis eĉ al homoj kun malmulte da kompreno pri la interna funkciado de fadenoj skribi aplikaĵojn, kiuj uzas ilin, aplikaĵojn kiuj ne frostas kiam ili faras longajn operaciojn. Uzi async/await estas temo por unu aŭ eĉ pluraj artikoloj, sed mi provos ricevi la esencon de ĝi en kelkaj frazoj:

  • async estas modifilo de metodo resenanta Taskon aŭ malplenon
  • kaj await estas ne-bloka Task-atendanta funkciigisto.

Denove: la await operatoro, en la ĝenerala kazo (estas esceptoj), liberigos la nunan fadenon de ekzekuto plu, kaj kiam la Tasko finos sian ekzekuton, kaj la fadenon (fakte, estus pli ĝuste diri la kuntekston). , sed pli pri tio poste) daŭrigos ekzekuti la metodon plu. Ene de .NET, ĉi tiu mekanismo estas efektivigita en la sama maniero kiel yield-reveno, kiam la skriba metodo iĝas tuta klaso, kiu estas ŝtatmaŝino kaj povas esti ekzekutita en apartaj pecoj depende de ĉi tiuj statoj. Iu ajn interesata povas skribi ajnan simplan kodon uzante asynс/wait, kompili kaj vidi la kunigon uzante JetBrains dotPeek kun Kompililo Generata Kodo ebligita.

Ni rigardu eblojn por lanĉi kaj uzi Task. En la kodekzemplo sube, ni kreas novan taskon, kiu faras nenion utilan (Fadeno.Sleep (10000)), sed en reala vivo ĉi tio devus esti iu kompleksa CPU-intensa laboro.

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
}

Tasko estas kreita kun kelkaj opcioj:

  • LongRunning estas sugesto, ke la tasko ne estos finita rapide, kio signifas, ke eble indas konsideri ne preni fadenon el la naĝejo, sed krei apartan por ĉi tiu Tasko por ne damaĝi aliajn.
  • AttachedToParent - Taskoj povas esti aranĝitaj en hierarkio. Se ĉi tiu opcio estis uzata, tiam la Tasko povas esti en stato, kie ĝi mem plenumis kaj atendas la ekzekuton de siaj infanoj.
  • PreferFairness - signifas ke estus pli bone ekzekuti Taskojn senditajn por ekzekuto pli frue antaŭ tiuj senditaj poste. Sed ĉi tio estas nur rekomendo kaj rezultoj ne estas garantiitaj.

La dua parametro pasita al la metodo estas CancellationToken. Por ĝuste trakti nuligon de operacio post kiam ĝi komenciĝis, la kodo ekzekutita devas esti plenigita per ĉekoj por la stato CancellationToken. Se ne estas kontroloj, tiam la Nuligi metodo vokita sur la CancellationTokenSource objekto povos ĉesigi la ekzekuton de la Tasko nur antaŭ ol ĝi komenciĝos.

La lasta parametro estas planilo objekto de tipo TaskScheduler. Ĉi tiu klaso kaj ĝiaj posteuloj estas dizajnitaj por kontroli strategiojn por distribuado de Taskoj trans fadenoj; defaŭlte, la Tasko estos efektivigita sur hazarda fadeno de la naĝejo.

La await-funkciigisto estas aplikata al la kreita Tasko, kio signifas, ke la kodo skribita post ĝi, se ekzistas unu, estos ekzekutita en la sama kunteksto (ofte tio signifas sur la sama fadeno) kiel la kodo antaŭ await.

La metodo estas markita kiel nesinkrona malplena, kio signifas, ke ĝi povas uzi la await-funkciigiston, sed la alvoka kodo ne povos atendi ekzekuton. Se tia funkcio estas necesa, tiam la metodo devas redoni Taskon. Metodoj markitaj nesinkrona malpleno estas sufiĉe oftaj: kiel regulo, ĉi tiuj estas evento-traktiloj aŭ aliaj metodoj, kiuj funkcias sur la fajro kaj forgeso-principo. Se vi devas ne nur doni la ŝancon atendi ĝis la fino de ekzekuto, sed ankaŭ redoni la rezulton, tiam vi devas uzi Task.

Sur la Tasko kiun la StartNew-metodo redonis, same kiel sur iu ajn alia, vi povas voki la ConfigureAwait-metodon kun la falsa parametro, tiam ekzekuto post atendado daŭros ne sur la kaptita kunteksto, sed sur arbitra. Ĉi tio ĉiam devas esti farita kiam la ekzekutkunteksto ne gravas por la kodo post atendado. Ĉi tio ankaŭ estas rekomendo de MS kiam vi verkas kodon, kiu estos liverita pakita en biblioteko.

Ni daŭras iom pli pri kiel vi povas atendi la kompletigon de Tasko. Malsupre estas ekzemplo de kodo, kun komentoj pri kiam la atendo estas farita kondiĉe bone kaj kiam ĝi estas farita kondiĉe malbone.

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
}

En la unua ekzemplo, ni atendas ke la Tasko finiĝos sen bloki la vokan fadenon; ni revenos al prilaborado de la rezulto nur kiam ĝi jam estas tie; ĝis tiam, la vokanta fadeno estas lasita al siaj propraj aparatoj.

En la dua opcio, ni blokas la vokan fadenon ĝis la rezulto de la metodo estas kalkulita. Ĉi tio estas malbona ne nur ĉar ni okupis fadenon, tian valoran rimedon de la programo, kun simpla senlaboreco, sed ankaŭ ĉar se la kodo de la metodo kiun ni nomas enhavas atendas, kaj la sinkroniga kunteksto postulas reveni al la vokanta fadeno post atendu, tiam ni ricevos blokiĝon : La alvokanta fadeno atendas la rezulton de la nesinkrona metodo por esti kalkulita, la nesinkrona metodo vane provas daŭrigi sian ekzekuton en la alvokanta fadeno.

Alia malavantaĝo de ĉi tiu aliro estas komplika erartraktado. La fakto estas, ke eraroj en nesinkrona kodo dum uzado de async/wait estas tre facile trakteblaj - ili kondutas same kvazaŭ la kodo estus sinkrona. Dum se ni aplikas sinkronan atendan ekzorcismon al Tasko, la origina escepto fariĝas AggregateException, t.e. Por trakti la escepton, vi devos ekzameni la InnerException tipon kaj skribi if-ĉenon mem ene de unu catch bloko aŭ uzi la catch kiam konstruo, anstataŭ la ĉeno de catch blokoj kiu estas pli konata en la C# mondo.

La tria kaj finaj ekzemploj ankaŭ estas markitaj malbonaj pro la sama kialo kaj enhavas ĉiujn samajn problemojn.

La metodoj WhenAny kaj WhenAll estas ekstreme oportunaj por atendi grupon de Taskoj; ili envolvas grupon de Taskoj en unu, kiu pafos aŭ kiam Tasko de la grupo unue estas ekigita, aŭ kiam ĉiuj el ili kompletigis sian ekzekuton.

Ĉesigi fadenojn

Pro diversaj kialoj, eble necesas ĉesigi la fluon post kiam ĝi komenciĝis. Estas kelkaj manieroj fari tion. La Fadena klaso havas du taŭge nomitajn metodojn: Aborto и Interrompi. La unua estas tre ne rekomendita por uzo, ĉar post vokado de ĝi en ajna hazarda momento, dum la prilaborado de iu instrukcio, escepto estos ĵetita ThreadAbortedException. Vi ne atendas tian escepton esti ĵetita kiam pliigo de ajna entjera variablo, ĉu ne? Kaj kiam vi uzas ĉi tiun metodon, ĉi tio estas tre reala situacio. Se vi bezonas malhelpi la CLR generi tian escepton en certa sekcio de kodo, vi povas envolvi ĝin per vokoj. Fadeno.BeginCriticalRegion, Fadeno.EndCriticalRegion. Ajna kodo skribita en finfine bloko estas envolvita en tiaj vokoj. Tial, en la profundo de la kadrokodo vi povas trovi blokojn kun malplena provo, sed ne malplena finfine. Mikrosofto malkuraĝigas ĉi tiun metodon tiom multe ke ili ne inkludis ĝin en .net-kerno.

La Interrompa metodo funkcias pli antaŭvideble. Ĝi povas interrompi la fadenon kun escepto ThreadInterruptedException nur dum tiuj momentoj, kiam la fadeno estas en atenda stato. Ĝi eniras ĉi tiun staton dum pendado atendante WaitHandle, ŝlosi, aŭ post vokado de Thread.Sleep.

Ambaŭ ebloj priskribitaj supre estas malbonaj pro ilia neantaŭvidebleco. La solvo estas uzi strukturon NuligoToken kaj klaso CancellationTokenSource. La afero estas jena: okazo de la klaso CancellationTokenSource estas kreita kaj nur tiu, kiu posedas ĝin, povas ĉesigi la operacion vokante la metodon. nuligi. Nur la CancellationToken estas transdonita al la operacio mem. Posedantoj de CancellationToken ne povas mem nuligi la operacion, sed nur povas kontroli ĉu la operacio estis nuligita. Estas Bulea propraĵo por ĉi tio EstasCancellationRequested kaj metodo ThrowIfCancelRequested. Ĉi-lasta ĵetos escepton TaskCancelledException se la Cancel-metodo estis vokita sur la CancellationToken-instanco estanta papago. Kaj ĉi tiu estas la metodo, kiun mi rekomendas uzi. Ĉi tio estas plibonigo super la antaŭaj elektoj akirante plenan kontrolon pri kiu punkto esceptoperacio povas esti ĉesigita.

La plej brutala opcio por ĉesigi fadenon estas voki la funkcion Win32 API TerminateThread. La konduto de la CLR post vokado de ĉi tiu funkcio povas esti neantaŭvidebla. Sur MSDN la jena estas skribita pri ĉi tiu funkcio: "TerminateThread estas danĝera funkcio, kiu devus esti uzata nur en la plej ekstremaj kazoj. “

Konverti heredan API al Taskbazita uzante FromAsync-metodon

Se vi bonŝancas labori pri projekto, kiu komenciĝis post kiam Taskoj estis enkondukitaj kaj ĉesis kaŭzi trankvilan teruron por la plej multaj programistoj, tiam vi ne devos trakti multajn malnovajn API-ojn, ambaŭ de triapartaj kaj tiuj de via teamo. torturis en la pasinteco. Feliĉe, la teamo de .NET Framework zorgis pri ni, kvankam eble la celo estis zorgi pri ni mem. Estu kiel ajn, .NET havas kelkajn ilojn por sendolore konverti kodon skribitan en malnovaj nesinkronaj programaj aliroj al la nova. Unu el ili estas la FromAsync-metodo de TaskFactory. En la kodekzemplo sube, mi envolvas la malnovajn nesinkronajn metodojn de la WebRequest-klaso en Tasko uzante ĉi tiun metodon.

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

Ĉi tio estas nur ekzemplo kaj vi verŝajne ne devos fari ĉi tion per enkonstruitaj tipoj, sed ajna malnova projekto simple plenas de BeginDoSomething-metodoj, kiuj resendas IAsyncResult kaj EndDoSomething-metodojn, kiuj ricevas ĝin.

Konverti heredan API al Taskbazita uzante TaskCompletionSource-klason

Alia grava ilo por konsideri estas la klaso TaskCompletionSource. Koncerne funkciojn, celo kaj principo de funkciado, ĝi povas esti iom rememoriga pri la RegisterWaitForSingleObject-metodo de la ThreadPool-klaso, pri kiu mi skribis supre. Uzante ĉi tiun klason, vi povas facile kaj oportune envolvi malnovajn nesinkronajn API-ojn en Taskoj.

Vi diros, ke mi jam parolis pri la metodo FromAsync de la klaso TaskFactory destinita por ĉi tiuj celoj. Ĉi tie ni devos memori la tutan historion de la disvolviĝo de nesinkronaj modeloj en .net, kiujn Microsoft proponis dum la pasintaj 15 jaroj: antaŭ la Task-Based Asynchronous Pattern (TAP), ekzistis la Nesinkrona Programado-Ŝablono (APP), kiu temis pri metodoj KomencuFaru io revenas IAsyncResult kaj metodoj FinoFaru Ion, kiu akceptas ĝin kaj por la heredaĵo de ĉi tiuj jaroj la FromAsync-metodo estas nur perfekta, sed kun la tempo, ĝi estis anstataŭigita per la Nesinkrona Ŝablono Bazita Eventa (KAJ AP), kiu supozis ke okazaĵo estus levita kiam la nesinkrona operacio finiĝis.

TaskCompletionSource estas perfekta por envolvi Taskojn kaj heredajn API-ojn konstruitajn ĉirkaŭ la eventa modelo. La esenco de ĝia laboro estas jena: objekto de ĉi tiu klaso havas publikan posedaĵon de tipo Task, kies stato povas esti kontrolita per la SetResult, SetException, ktp.-metodoj de la TaskCompletionSource klaso. En lokoj kie la atendu operatoro estis aplikita al ĉi tiu Tasko, ĝi estos efektivigita aŭ malsukcesos kun escepto depende de la metodo aplikita al la TaskCompletionSource. Se ĝi ankoraŭ ne klaras, ni rigardu ĉi tiun kodekzemplon, kie iu malnova EAP-API estas envolvita en Tasko uzante TaskCompletionSource: kiam la evento ekfunkciiĝos, la Tasko estos metita en la Finitan staton, kaj la metodon kiu aplikis la atendu operatoron. al ĉi tiu Tasko rekomencos sian ekzekuton ricevinte la objekton rezulto.

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 Konsiloj kaj Trukoj

Envolvi malnovajn API-ojn ne estas ĉio, kio povas esti farita per TaskCompletionSource. Uzado de ĉi tiu klaso malfermas interesan eblecon desegni diversajn API-ojn pri Taskoj, kiuj ne okupas fadenojn. Kaj la rivereto, kiel ni memoras, estas multekosta rimedo kaj ilia nombro estas limigita (ĉefe per la kvanto de RAM). Ĉi tiu limigo povas esti facile atingita disvolvante, ekzemple, ŝarĝitan TTT-aplikaĵon kun kompleksa komerca logiko. Ni konsideru la eblecojn, pri kiuj mi parolas, kiam oni efektivigas tian lertaĵon kiel Long-Polling.

Resume, la esenco de la lertaĵo estas ĉi tio: vi devas ricevi informojn de la API pri iuj eventoj okazantaj sur ĝia flanko, dum la API, ial, ne povas raporti la eventon, sed nur povas redoni la staton. Ekzemplo de ĉi tiuj estas ĉiuj API-oj konstruitaj sur HTTP antaŭ la tempoj de WebSocket aŭ kiam estis neeble ial uzi ĉi tiun teknologion. La kliento povas demandi la HTTP-servilon. La HTTP-servilo ne povas mem iniciati komunikadon kun la kliento. Simpla solvo estas sondi la servilon uzante tempigilon, sed ĉi tio kreas plian ŝarĝon sur la servilo kaj plian prokraston averaĝe TimerInterval / 2. Por ĉirkaŭiri ĉi tion, oni inventis lertaĵon nomita Long Polling, kiu implikas prokrasti la respondon de la servilo ĝis la Tempo eksvalidiĝos aŭ okazos evento. Se la evento okazis, tiam ĝi estas procesita, se ne, tiam la peto estas sendita denove.

while(!eventOccures && !timeoutExceeded)  {

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

Sed tia solvo montriĝos terura tuj kiam pliiĝos la nombro da klientoj atendantaj la eventon, ĉar... Ĉiu tia kliento okupas tutan fadenon atendante eventon. Jes, kaj ni ricevas plian 1ms prokraston kiam la evento estas ekigita, plej ofte ĉi tio ne estas signifa, sed kial fari la programaron pli malbona ol ĝi povas esti? Se ni forigas Thread.Sleep(1), tiam vane ni ŝarĝos unu procesoran kernon 100% neaktiva, turniĝanta en senutila ciklo. Uzante TaskCompletionSource vi povas facile refari ĉi tiun kodon kaj solvi ĉiujn problemojn identigitajn supre:

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

Ĉi tiu kodo ne estas produktadpreta, sed nur demo. Por uzi ĝin en realaj kazoj, vi ankaŭ bezonas, minimume, trakti la situacion kiam mesaĝo alvenas en tempo, kiam neniu atendas ĝin: en ĉi tiu kazo, la metodo AsseptMessageAsync devus resendi jam plenumitan Taskon. Se ĉi tio estas la plej ofta kazo, tiam vi povas pensi pri uzi ValueTask.

Kiam ni ricevas peton por mesaĝo, ni kreas kaj metas TaskCompletionSource en la vortaron, kaj tiam atendas kio okazas unue: la specifita tempintervalo eksvalidiĝas aŭ mesaĝo estas ricevita.

ValueTask: kial kaj kiel

La asinkronaj/atendantaj operatoroj, kiel la rendimento-revena operatoro, generas ŝtatmaŝinon el la metodo, kaj ĉi tio estas la kreado de nova objekto, kiu preskaŭ ĉiam ne gravas, sed en maloftaj kazoj ĝi povas krei problemon. Ĉi tiu kazo povas esti metodo kiu estas nomita vere ofte, ni parolas pri dekoj kaj centoj da miloj da vokoj sekundo. Se tia metodo estas skribita tiel ke en la plej multaj kazoj ĝi resendas rezulton preterpasante ĉiujn atendatajn metodojn, tiam .NET provizas ilon por optimumigi ĉi tion - la ValueTask-strukturo. Por klarigi ĝin, ni rigardu ekzemplon de ĝia uzo: estas kaŝmemoro, al kiu ni tre ofte iras. Estas iuj valoroj en ĝi kaj tiam ni simple resendas ilin; se ne, tiam ni iras al iu malrapida IO por akiri ilin. Mi volas fari ĉi-lastan nesinkrone, kio signifas, ke la tuta metodo montriĝas nesinkrona. Tiel, la evidenta maniero skribi la metodon estas kiel sekvas:

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

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

Pro la deziro iom optimumigi, kaj iometa timo pri tio, kion Roslyn generos dum la kompilo de ĉi tiu kodo, vi povas reverki ĉi tiun ekzemplon jene:

public Task<string> GetById(int id) {

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

Efektive, la optimuma solvo en ĉi tiu kazo estus optimumigi la varman vojon, nome, ricevi la valoron de la vortaro sen iuj nenecesaj asignoj kaj ŝarĝi sur la GC, dum en tiuj maloftaj kazoj kiam ni ankoraŭ bezonas iri al IO por datumoj. , ĉio restos pli/minus la malnova maniero:

public ValueTask<string> GetById(int id) {

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

Ni rigardu pli detale ĉi tiun kodon: se estas valoro en la kaŝmemoro, ni kreas strukturon, alie la reala tasko estos envolvita en signifoplena. La alvoka kodo ne zorgas en kiu vojo ĉi tiu kodo estis ekzekutita: ValueTask, de C# sintaksa vidpunkto, kondutos same kiel regula Task en ĉi tiu kazo.

TaskSchedulers: administrado de taskaj lanĉstrategioj

La sekva API, kiun mi ŝatus konsideri, estas la klaso TaskScheduler kaj ĝiaj derivaĵoj. Mi jam menciis supre, ke TPL havas la kapablon administri strategiojn por distribui Taskojn tra fadenoj. Tiaj strategioj estas difinitaj en la posteuloj de la TaskScheduler klaso. Preskaŭ ajna strategio, kiun vi eble bezonos, troviĝas en la biblioteko. ParallelExtensionsExtras, evoluigita fare de Mikrosofto, sed ne parto de .NET, sed liverita kiel Nuget-pakaĵo. Ni mallonge rigardu kelkajn el ili:

  • Current ThreadTaskScheduler — efektivigas Taskojn sur la nuna fadeno
  • LimitedConcurrencyLevelTaskScheduler — limigas la nombron da Taskoj ekzekutitaj samtempe per parametro N, kiu estas akceptita en la konstrukciisto
  • Ordigita TaskScheduler — estas difinita kiel LimitedConcurrencyLevelTaskScheduler(1), do taskoj estos plenumitaj sinsekve.
  • WorkStealingTaskScheduler - iloj laboro-ŝtelado alproksimiĝo al taskodistribuo. Esence ĝi estas aparta ThreadPool. Solvas la problemon, ke en .NET ThreadPool estas statika klaso, unu por ĉiuj aplikoj, kio signifas, ke ĝia troŝarĝo aŭ malĝusta uzo en unu parto de la programo povas konduki al kromefikoj en alia. Krome, estas ege malfacile kompreni la kaŭzon de tiaj difektoj. Tio. Povas esti bezono uzi apartajn WorkStealingTaskSchedulers en partoj de la programo kie la uzo de ThreadPool povas esti agresema kaj neantaŭvidebla.
  • QueuedTaskScheduler — permesas al vi plenumi taskojn laŭ reguloj pri prioritataj atendovicoj
  • ThreadPerTaskScheduler — kreas apartan fadenon por ĉiu Tasko kiu estas efektivigita sur ĝi. Povas esti utila por taskoj, kiuj bezonas neantaŭvideble longan tempon por plenumi.

Estas bona detala artikolo pri TaskSchedulers en la mikrosofta blogo.

Por oportuna senararigado de ĉio rilata al Taskoj, Visual Studio havas Task-fenestron. En ĉi tiu fenestro vi povas vidi la nunan staton de la tasko kaj salti al la nune plenumanta linio de kodo.

.NET: Iloj por labori kun multfadenado kaj malsinkronio. Parto 1

PLinq kaj la Paralela klaso

Krom Taskoj kaj ĉio dirita pri ili, estas du pli interesaj iloj en .NET: PLinq (Linq2Parallel) kaj la Parallel klaso. La unua promesas paralelan plenumon de ĉiuj Linq-operacioj sur multoblaj fadenoj. La nombro da fadenoj povas esti agordita per la metodo de etendaĵo WithDegreeOfParallelism. Bedaŭrinde, plej ofte PLinq en sia defaŭlta reĝimo ne havas sufiĉajn informojn pri la internoj de via datumfonto por provizi signifan rapidecan gajnon, aliflanke, la kosto de provi estas tre malalta: vi nur bezonas voki la metodon AsParallel antaŭe. la ĉeno de Linq-metodoj kaj ruli elfarajn testojn. Plie, eblas transdoni pliajn informojn al PLinq pri la naturo de via datumfonto uzante la mekanismon Dispartigoj. Vi povas legi pli tie и tie.

La Paralela senmova klaso disponigas metodojn por ripetadi tra Foreach-kolekto paralele, ekzekuti For-buklon kaj ekzekuti plurajn delegitojn paralele Invoke. Ekzekuto de la nuna fadeno estos ĉesigita ĝis la kalkuloj finiĝos. La nombro da fadenoj povas esti agordita pasante ParallelOptions kiel la lasta argumento. Vi ankaŭ povas specifi TaskScheduler kaj CancellationToken uzante opciojn.

trovoj

Kiam mi komencis verki ĉi tiun artikolon surbaze de la materialoj de mia raporto kaj la informoj, kiujn mi kolektis dum mia laboro post ĝi, mi ne atendis, ke estos tiom da ĝi. Nun, kiam la tekstredaktilo, en kiu mi tajpas ĉi tiun artikolon, riproĉe diros al mi, ke paĝo 15 malaperis, mi resumos la provizorajn rezultojn. Aliaj lertaĵoj, APIoj, vidaj iloj kaj malfacilaĵoj estos kovritaj en la sekva artikolo.

Konkludoj:

  • Vi devas koni la ilojn por labori kun fadenoj, malsinkronio kaj paraleleco por uzi la rimedojn de modernaj komputiloj.
  • .NET havas multajn malsamajn ilojn por ĉi tiuj celoj
  • Ne ĉiuj aperis samtempe, do vi ofte povas trovi heredaĵajn, tamen ekzistas manieroj konverti malnovajn API-ojn sen multe da peno.
  • Labori kun fadenoj en .NET estas reprezentata de la klasoj Thread kaj ThreadPool
  • La metodoj Thread.Abort, Thread.Interrupt kaj Win32 API TerminateThread estas danĝeraj kaj ne rekomendindaj por uzo. Anstataŭe, estas pli bone uzi la mekanismon CancellationToken
  • Fluo estas valora rimedo kaj ĝia provizo estas limigita. Situacioj kie fadenoj estas okupataj atendante eventojn devus esti evititaj. Por tio estas oportune uzi la klason TaskCompletionSource
  • La plej potencaj kaj altnivelaj .NET-iloj por labori kun paraleleco kaj malsinkronio estas Taskoj.
  • La c# async/wait-funkciigistoj efektivigas la koncepton de ne-bloka atendo
  • Vi povas kontroli la distribuadon de Taskoj tra fadenoj uzante klasojn derivitajn de TaskScheduler
  • La ValueTask-strukturo povas esti utila en optimumigo de varmaj vojoj kaj memortrafiko
  • La fenestroj Taskoj kaj Fadenoj de Visual Studio provizas multajn informojn utilajn por sencimigi multfadenan aŭ nesinkronan kodon
  • PLinq estas bonega ilo, sed ĝi eble ne havas sufiĉajn informojn pri via datumfonto, sed ĉi tio povas esti riparita per la dispartiga mekanismo.
  • Daŭrigota…

fonto: www.habr.com

Aldoni komenton