.NET: Strumenti per travaglià cù multithreading è asincronia. Parte 1

Aghju publicatu l'articulu originale nantu à Habr, a traduzzione di quale hè publicata in a corporazione blog post.

A necessità di fà qualcosa in modu asincronu, senza aspittà per u risultatu quì è avà, o di sparte un grande travagliu trà parechje unità chì l'eseguinu, esisteva prima di l'avventu di l'urdinatori. Cù u so avventu, sta necessità hè diventata assai tangibule. Avà, in 2019, scrivite stu articulu nantu à un laptop cù un processore Intel Core di 8 core, nantu à quale più di centu prucessi sò in parallelu, è ancu più fili. Vicinu, ci hè un telefuninu un pocu struitu, compru un paru d'anni fà, hà un processore 8-core à bordu. I risorsi tematichi sò pieni di articuli è video induve i so autori ammiranu i smartphones di punta di questu annu chì presentanu processori 16-core. MS Azure furnisce una macchina virtuale cù un processore core 20 è 128 TB RAM per menu di $ 2 / ora. Sfurtunatamente, hè impussibile d'estrattà u massimu è sfruttà sta putenza senza pudè gestisce l'interazzione di filamenti.

Terminologia

Prucessu - Oggettu OS, spaziu di indirizzu isolatu, cuntene fili.
Filu - un ughjettu OS, a più chjuca unità di esecutivu, parte di un prucessu, i fili sparte memoria è altre risorse trà elli in un prucessu.
Multitasking - Proprietà OS, a capacità di eseguisce parechji prucessi simultaneamente
Multi-core - una pruprietà di u processatore, a capacità di utilizà parechji core per u processu di dati
Multiprocessing - una pruprietà di un urdinatore, a capacità di travaglià simultaneamente cù parechji prucessori fisicamenti
Multithreading - una pruprietà di un prucessu, a capacità di distribuisce u prucessu di dati trà parechji fili.
Parallelismu - realizà parechje azzioni fisicamente simultaneamente per unità di tempu
Asincronia - esecuzione di una operazione senza aspittà à a fine di stu prucessu; u risultatu di l'esekzione pò esse processatu dopu.

Metafora

Micca tutte e definizioni sò boni è certi anu bisognu di spiegazione supplementu, cusì aghjustà una metàfora nantu à a cucina di colazione à a terminologia formalmente introdutta. A cucina di colazione in questa metàfora hè un prucessu.

Mentre preparava u colazione in a mattina I (CPU) Vengu in cucina (Computer). Aghju 2 mani (scorci). Ci hè una quantità di dispusitivi in ​​a cucina (IO): fornu, bollitore, toaster, frigo. Accendi u gasu, mette una padedda nantu à ellu è versà l'oliu in questu senza aspittà chì si scalda (asynchronously, Non-Blocking-IO-Wait), Pigliu l'ova da a frigorifera è li rumpiu in un platu, dopu batte cù una manu (Thread #1), è secondu (Thread #2) tenendu u piattu (Risorsa Shared). Avà vogliu accende u bollitore, ma ùn aghju micca abbastanza mani (Fame di filu) Duranti stu tempu, a padedda calienta (Processing the result) in quale aghju pour ciò chì aghju battutu. Aghjunghjite u bollitore è l'accendo è fighjulu stupidu l'acqua chì bolli in ellu (Blocking-IO-Wait), ancu s'ellu durante stu tempu puderia lavà u platu duv'ellu batteva l'omelette.

Aghju cottu una frittata cù solu 2 mani, è ùn aghju micca più, ma à u stessu tempu, à u mumentu di sbattà l'omelette, 3 operazioni sò state realizate à una volta: sbattà l'omelette, tenendu u piattu, scaldendu a padedda. U CPU hè a parte più veloce di l'urdinatore, IO hè ciò chì hè più spessu tuttu rallenta, cusì spessu una suluzione efficace hè di occupà u CPU cù qualcosa mentre riceve dati da IO.

Cuntinuà a metafora:

  • Sè in u prucessu di preparazione di una frittata, avissi ancu pruvà à cambià a ropa, questu seria un esempiu di multitasking. Una sfumatura impurtante: l'urdinatori sò assai megliu in questu chì e persone.
  • Una cucina cù parechji chefs, per esempiu in un ristorante - un computer multi-core.
  • Parechje ristoranti in un food court in un centru cummerciale - data center

Strumenti .NET

.NET hè bonu per travaglià cù filamenti, cum'è cù parechje altre cose. Cù ogni nova versione, introduce più è più novi strumenti per travaglià cun elli, novi strati di astrazione nantu à i fili OS. Quandu u travagliu cù a custruzzione di astrazioni, i sviluppatori di u framework utilizanu un approcciu chì lascia l'uppurtunità, quandu si usa una astrazione d'altu livellu, per falà unu o più livelli sottu. A maiò spessu, questu ùn hè micca necessariu, in fattu, apre a porta per sparà in u pede cù una fucile, ma qualchì volta, in casi rari, pò esse l'unicu modu per risolve un prublema chì ùn hè micca risoltu à u livellu attuale di astrazione. .

Per arnesi, vogliu dì sia l'interfaccia di prugrammazione di l'applicazioni (API) furnite da u framework è i pacchetti di terzu, è ancu solu solu solu software chì simplificà a ricerca di qualsiasi prublemi ligati à u codice multi-threaded.

Cumincià un filu

A classa Thread hè a classa più basica in .NET per travaglià cù i filamenti. U custruttore accetta unu di dui delegati:

  • ThreadStart - Nisun paràmetru
  • ParametrizedThreadStart - cù un paràmetru di ughjettu tipu.

U delegatu serà eseguitu in u filu novu creatu dopu avè chjamatu u metudu Start.Se un delegatu di tipu ParametrizedThreadStart hè statu passatu à u custruttore, allora un ughjettu deve esse passatu à u metudu Start. Stu mecanismu hè necessariu per trasfiriri ogni infurmazione lucale à u flussu. Hè da nutà chì a creazione di un filu hè una operazione caru, è u filu stessu hè un ughjettu pesante, almenu perchè attribuisce 1MB di memoria nantu à a pila è esige interazzione cù l'API OS.

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

A classe ThreadPool rapprisenta u cuncettu di una piscina. In .NET, u filu di filu hè un pezzu di ingegneria, è i sviluppatori di Microsoft anu fattu assai sforzu per assicurà chì u travagliu ottimali in una larga varietà di scenarii.

Cuncepimentu generale:

Da u mumentu chì l'applicazione principia, crea parechji fili in riserva in u fondu è furnisce a capacità di piglià per l'usu. Se i fili sò usati spessu è in grande quantità, a piscina si espansione per risponde à i bisogni di u chjamante. Quandu ùn ci sò micca fili gratuiti in a piscina à u mumentu propiu, o aspittàrà chì unu di i fili torna, o creà un novu. Ne segue chì u filu di filu hè grande per alcune azzioni di cortu termine è pocu adattatu per l'operazioni chì funzionanu cum'è servizii in tuttu u funziunamentu di l'applicazione.

Per utilizà un filu da a piscina, ci hè un metudu QueueUserWorkItem chì accetta un delegatu di u tipu WaitCallback, chì hà a listessa firma cum'è ParametrizedThreadStart, è u paràmetru passatu à ellu faci a listessa funzione.

ThreadPool.QueueUserWorkItem(...);

U metudu di pool di fili menu cunnisciutu RegisterWaitForSingleObject hè utilizatu per urganizà operazioni IO senza bloccu. U delegatu passatu à stu metudu serà chjamatu quandu u WaitHandle passatu à u metudu hè "Released".

ThreadPool.RegisterWaitForSingleObject(...)

.NET hà un timer di filu è si differenzia da i timers WinForms / WPF in chì u so handler serà chjamatu nantu à un filu pigliatu da a piscina.

System.Threading.Timer

Ci hè ancu un modu piuttostu esoticu per mandà un delegatu per eseguisce à un filu da a piscina - u metudu BeginInvoke.

DelegateInstance.BeginInvoke

Vogliu spiegà brevemente a funzione à quale parechji di i metudi di sopra ponu esse chjamati - CreateThread da Kernel32.dll Win32 API. Ci hè una manera, grazia à u mecanismu di metudi esterni, per chjamà sta funzione. Aghju vistu una tale chjama solu una volta in un terribili esempiu di codice legatu, è a motivazione di l'autore chì hà fattu esattamente questu ferma sempre un misteru per mè.

Kernel32.dll CreateThread

Visualizazione è Debugging Threads

Threads creati da voi, tutti i cumpunenti di terzu-party, è a piscina .NET pò esse vistu in a finestra Threads di Visual Studio. Questa finestra mostrarà solu l'infurmazioni di filu quandu l'applicazione hè in debug è in modalità Break. Quì pudete vede convenientemente i nomi di pila è e priorità di ogni filu, è cambià a debugging à un filu specificu. Utilizendu a pruprietà Priorità di a classa Thread, pudete stabilisce a priorità di un filu, chì l'OC è CLR perciveranu cum'è una ricunniscenza quandu si divide u tempu di u processatore trà i filamenti.

.NET: Strumenti per travaglià cù multithreading è asincronia. Parte 1

Task Biblioteca Parallela

Task Parallel Library (TPL) hè statu introduttu in .NET 4.0. Avà hè u standard è u principale strumentu per travaglià cù asincronia. Ogni codice chì usa un approcciu più anticu hè cunsideratu legatu. L'unità basica di TPL hè a classa Task da u spaziu di nomi System.Threading.Tasks. Un compitu hè una astrazione nantu à un filu. Cù a nova versione di a lingua C#, avemu un modu eleganti di travaglià cù Tasks - operatori async/wait. Questi cuncetti anu permessu di scrive codice asincronu cum'è s'ellu era simplice è sincronu, questu hà permessu ancu per e persone cun poca cunniscenza di u funziunamentu internu di i fili di scrive l'applicazioni chì l'utilizanu, l'applicazioni chì ùn si congelanu micca quandu facenu operazioni longu. Utilizà l'async/wait hè un tema per unu o ancu parechji articuli, ma pruvaraghju à ottene l'essenza di questu in uni pochi di frasi:

  • async hè un modificatore di un metudu chì torna Task o void
  • è await hè un operatore in attesa di Task senza bloccu.

Una volta di più: l'operatore d'attesa, in u casu generale (ci sò eccezzioni), liberarà u filu attuale di l'esekzione in più, è quandu u Task finisce a so esecuzione, è u filu (in fatti, saria più currettu per dì u cuntestu). , ma più nantu à questu dopu) continuerà à eseguisce u metudu in più. Dentru .NET, stu miccanisimu hè implementatu in u listessu modu cum'è u rendimentu di ritornu, quandu u metudu scrittu si trasforma in una classa sana, chì hè una macchina statale è pò esse eseguita in pezzi separati sicondu sti stati. Qualchese interessatu pò scrive qualsiasi codice simplice usendu asynс/wait, compile è vede l'assemblea utilizendu JetBrains dotPeek cù u Codice Generatu Compiler attivatu.

Fighjemu l'opzioni per lancià è aduprà Task. In l'esempiu di codice quì sottu, creemu una nova attività chì ùn faci nunda d'utile (Thread.Sleep (10000)), ma in a vita reale questu deve esse un travagliu cumplessu intensivu di CPU.

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
}

Un Task hè creatu cù parechje opzioni:

  • LongRunning hè un suggerimentu chì u compitu ùn serà micca cumpletu rapidamente, chì significa chì pò esse degne di cunsiderà micca piglià un filu da a piscina, ma creanu un separatu per questa Task per ùn dannà l'altri.
  • AttachedToParent - I travaglii ponu esse disposti in una ghjerarchia. Sè sta opzione hè stata aduprata, allura u Task pò esse in un statu induve ellu stessu hà finitu è ​​aspetta l'esekzione di i so figlioli.
  • PreferFairness - significa chì saria megliu eseguisce Tasks mandati per l'esekzione prima prima di quelli mandati dopu. Ma questu hè solu una raccomandazione è i risultati ùn sò micca garantiti.

U sicondu paràmetru passatu à u metudu hè CancellationToken. Per trattà currettamente l'annullamentu di una operazione dopu avè principiatu, u codice chì esse eseguitu deve esse cumpletu di cuntrolli per u Statu CancellationToken. Se ùn ci sò micca cuntrolli, allura u metudu di annullamentu chjamatu annantu à l'ughjettu CancellationTokenSource puderà piantà l'esekzione di a Task solu prima di principià.

L'ultimu paràmetru hè un ughjettu pianificatore di tipu TaskScheduler. Questa classa è i so discendenti sò pensati per cuntrullà e strategie per a distribuzione di Tasks à traversu i fili; per automaticamente, u Task serà eseguitu nantu à un filu aleatoriu da u pool.

L'operatore await hè appiicatu à u Task creatu, chì significa chì u codice scrittu dopu, s'ellu ci hè unu, serà eseguitu in u stessu cuntestu (spessu significa nantu à u stessu filu) cum'è u codice prima di aspittà.

U metudu hè marcatu cum'è async void, chì significa chì pò aduprà l'operatore await, ma u codice chjamatu ùn serà micca capaci di aspittà per l'esekzione. Sè una tale funzione hè necessariu, allura u metudu deve vultà Task. I metudi marcati async void sò abbastanza cumuni: cum'è regula, questi sò gestori di avvenimenti o altri metudi chì travaglianu nantu à u principiu di u focu è scurdate. Sè avete bisognu di micca solu dà l'uppurtunità di aspittà finu à a fine di l'esekzione, ma dinò di rinvià u risultatu, allora avete bisognu di utilizà Task.

Nant'à u Task chì u metudu StartNew hà tornatu, è ancu nantu à qualsiasi altru, pudete chjamà u metudu ConfigureAwait cù u falsu paràmetru, dopu l'esekzione dopu l'attesa continuarà micca in u cuntestu catturatu, ma in un arbitrariu. Questu deve esse sempre fattu quandu u cuntestu di esicuzzioni ùn hè micca impurtante per u codice dopu aspittà. Questu hè ancu una ricunniscenza da MS quandu scrive u codice chì serà furnitu imballatu in una biblioteca.

Andemu un pocu di più nantu à cumu pudete aspittà à a fine di una Task. Quì sottu hè un esempiu di codice, cù cumenti nantu à quandu l'aspettazione hè fatta cundizionalmente bè è quandu hè fatta cundizionalmente male.

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
}

In u primu esempiu, aspittemu chì a Task finisci senza bluccà u filu di chjama; torneremu à trasfurmà u risultatu solu quandu ci hè digià; finu à quì, u filu di chjama hè lasciatu à i so dispositi.

In a seconda opzione, bluccà u filu chjamatu finu à chì u risultatu di u metudu hè calculatu. Questu hè male micca solu perchè avemu occupatu un filu, una risorsa cusì preziosa di u prugramma, cù simplicità oziosa, ma ancu perchè se u codice di u metudu chì chjamemu cuntene aspittà, è u cuntestu di sincronizazione hà bisognu di vultà à u filu chjamatu dopu. aspetta, allora averemu un bloccu : U filu chjamatu aspetta chì u risultatu di u metudu asincronu sia calculatu, u metudu asincronu prova in vain à cuntinuà a so esecuzione in u filu chjamatu.

Un altru svantaghju di stu approcciu hè a gestione di l'errore complicata. U fattu hè chì l'errore in u codice asincronu quandu si usa l'async/wait sò assai faciuli di trattà - si cumportanu cum'è se u codice era sincronu. Mentre chì se applichemu l'esorcismo di attesa sincrona à una Task, l'eccezzioni originale si trasforma in una AggregateException, i.e. Per trattà l'eccezzioni, duverete esaminà u tippu InnerException è scrivite una catena se sè stessu in un bloccu di catch o utilizate a catch when constructing, invece di a catena di catch blocks chì hè più familiar in u mondu C#.

U terzu è l'esempiu finali sò ancu marcati male per a listessa ragione è cuntenenu tutti i stessi prublemi.

I metudi WhenAny è WhenAll sò estremamente convenienti per aspittà per un gruppu di Tasks; imboccanu un gruppu di Tasks in unu, chì spararà sia quandu un Task da u gruppu hè attivatu prima, o quandu tutti anu finitu a so esecuzione.

Firmà i fili

Per diversi motivi, pò esse necessariu di piantà u flussu dopu avè principiatu. Ci hè parechje manere di fà questu. A classe Thread hà dui metudi chjamati appropritamente: Aborti и Interruzzione. U primu ùn hè assai cunsigliatu per l'usu, perchè dopu avè chjamatu in ogni mumentu aleatoriu, durante u processu di qualsiasi struzzione, una eccezzioni serà ghjittata ThreadAbortedException. Ùn aspettate micca chì una tale eccezzioni sia lanciata quandu si aumenta ogni variabile intera, nò? È quandu si usa stu metudu, hè una situazione assai reale. Sè avete bisognu di impedisce à u CLR di generà una tale eccezzioni in una certa sezione di codice, pudete imballà in chjama. Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Ogni codice scrittu in un bloccu infine hè impannillatu in tali chjama. Per questu mutivu, in a prufundità di u codice quadru pudete truvà blocchi cù una prova viota, ma micca un viotu infine. Microsoft scoraghja stu metudu tantu chì ùn l'anu micca inclusu in .net core.

U metudu Interrupt funziona più previsiblemente. Pò interrompe u filu cù una eccezzioni ThreadInterruptedException solu in quelli mumenti quandu u filu hè in un statu d'aspittà. Si entre in stu statu, mentri impiccatu mentri aspittava di WaitHandle, serratura, o dopu à chjamà Thread.Sleep.

E duie opzioni descritte sopra sò cattivi per via di a so imprevisibilità. A suluzione hè di utilizà una struttura CancellationToken è classa CancellationTokenSource. U puntu hè questu: una istanza di a classa CancellationTokenSource hè creata è solu quellu chì a pussede pò piantà l'operazione chjamendu u metudu Annulla. Solu u CancellationToken hè passatu à l'operazione stessu. I pruprietarii di CancellationToken ùn ponu micca annullà l'operazione elli stessi, ma ponu verificà solu se l'operazione hè stata annullata. Ci hè una pruprietà booleana per questu IsCancellationRequested è u metudu ThrowIfCancelRequested. L'ultime lancerà una eccezzioni TaskCancelledException se u metudu di annullamentu hè statu chjamatu annantu à l'istanza CancellationToken chì hè stata parrodata. È questu hè u metudu chì ricumandemu di utilizà. Questa hè una mellura annantu à l'opzioni precedenti per ottene u cuntrollu tutale nantu à quale puntu una operazione d'eccezzioni pò esse abortita.

L'opzione più brutale per fermà un filu hè di chjamà a funzione Win32 API TerminateThread. U cumpurtamentu di u CLR dopu à chjamà sta funzione pò esse imprevisible. Nant'à MSDN u seguente hè scrittu annantu à sta funzione: "TerminateThread hè una funzione periculosa chì deve esse aduprata solu in i casi più estremi. "

Cunvertisce l'API legacy in Task Based utilizendu u metudu FromAsync

Sè avete a furtuna di travaglià in un prughjettu chì hè statu iniziatu dopu à l'introduzione di Tasks è hà cessatu di causà orrore tranquillu per a maiò parte di i sviluppatori, allora ùn avete micca bisognu di trattà cun assai vechji API, sia quelli di terzu è quelli di a vostra squadra. hà torturatu in u passatu. Per furtuna, a squadra di .NET Framework hà pigliatu cura di noi, ancu s'è forse u scopu era di piglià cura di noi. Sia com'è, .NET hà una quantità di arnesi per cunvertisce senza dolore u codice scrittu in vechji avvicinamenti di prugrammazione asincrona à u novu. Unu di elli hè u metudu FromAsync di TaskFactory. In l'esempiu di codice sottu, aghju invucatu i vechji metudi async di a classa WebRequest in una Task cù stu metudu.

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

Questu hè solu un esempiu è hè improbabile di avè da fà questu cù tipi integrati, ma ogni vechju prughjettu hè simplicemente sbulicatu cù i metudi BeginDoSomething chì tornanu i metudi IAsyncResult è EndDoSomething chì u ricevenu.

Cunvertite l'API legacy in Task Based usendu a classe TaskCompletionSource

Un altru strumentu impurtante per cunsiderà hè a classa TaskCompletionSource. In quantu à e funzioni, u scopu è u principiu di u funziunamentu, pò esse un pocu reminiscent di u metudu RegisterWaitForSingleObject di a classa ThreadPool, chì aghju scrittu sopra. Aduprendu sta classa, pudete facilmente è comodamente imballà vechji API asincroni in Tasks.

Diterete chì aghju digià parlatu di u metudu FromAsync di a classa TaskFactory destinata à questi scopi. Quì avemu da ricurdà tutta a storia di u sviluppu di mudelli asincroni in .net chì Microsoft hà prupostu annantu à l'ultimi 15 anni: prima di u Task-Based Asynchronous Pattern (TAP), ci era u Pattern di Programmazione Asynchronous (APP), chì era nantu à i metudi CuminciàFà qualcosa chì torna IAsyncResult è metudi EndDoSomething chì l'accetta è per l'eredità di questi anni u metudu FromAsync hè solu perfettu, ma cù u tempu, hè statu rimpiazzatu da u Pattern Asynchronous Basatu in Eventi (È AP), chì assume chì un avvenimentu seria risuscitatu quandu l'operazione asincrona hè cumpletata.

TaskCompletionSource hè perfettu per imballà Tasks è API legacy custruite intornu à u mudellu di l'avvenimentu. L'essenza di u so travagliu hè a siguenti: un ughjettu di sta classa hà una pruprietà publica di tipu Task, u statu di quale pò esse cuntrullatu per mezu di i metudi SetResult, SetException, etc. di a classa TaskCompletionSource. In i lochi induve l'operatore d'attesa hè statu appiicatu à questa Task, serà eseguitu o falla cù una eccezzioni secondu u metudu appiicatu à TaskCompletionSource. S'ellu ùn hè ancu chjaru, fighjemu questu esempiu di codice, induve qualchì vechju API EAP hè impannillatu in una Task usendu un TaskCompletionSource: quandu l'avvenimentu spara, u Task serà trasferitu à u Statu Cumplitu, è u metudu chì applicà l'operatore d'attesa. à sta Task ripiglià a so esicuzzioni avendu ricevutu l'ughjettu risurtatu.

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 Tips & Trucchi

L'imballaggio di API antichi ùn hè micca tuttu ciò chì pò esse fattu cù TaskCompletionSource. L'usu di sta classa apre una interessante pussibilità di cuncepisce diverse API in Tasks chì ùn occupanu micca fili. È u flussu, cum'è ricurdate, hè un risorsu caru è u so numeru hè limitatu (principalmente da a quantità di RAM). Questa limitazione pò esse facilmente ottenuta da u sviluppu, per esempiu, una applicazione web caricata cù una logica cummerciale cumplessa. Cunsideremu e pussibulità chì parlu quandu implementanu un truccu cum'è Long-Polling.

In corta, l'essenza di u truccu hè questu: avete bisognu di riceve infurmazione da l'API nantu à certi avvenimenti chì si sò accaduti da u so latu, mentre chì l'API, per una certa ragione, ùn pò micca signalà l'avvenimentu, ma pò solu rinvià u statu. Un esempiu di queste sò tutte l'API custruite nantu à HTTP prima di i tempi di WebSocket o quandu era impussibile per una certa ragione di utilizà sta tecnulugia. U cliente pò dumandà à u servitore HTTP. U servitore HTTP ùn pò micca inizà a cumunicazione cù u cliente. Una solu suluzione simplice hè di sondaghju u servitore utilizendu un cronometru, ma questu crea una carica supplementu nantu à u servitore è un ritardu supplementu in media TimerInterval / 2. Per aggirari questu, hè statu inventatu un truccu chjamatu Long Polling, chì implica ritardà a risposta da u servitore finu à chì u Timeout scade o un avvenimentu accadirà. Se l'avvenimentu hè accadutu, allora hè trattatu, se no, a dumanda hè mandata di novu.

while(!eventOccures && !timeoutExceeded)  {

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

Ma una tale suluzione pruverà à esse terribili appena u numeru di clienti chì aspetta l'avvenimentu aumenta, perchè ... Ogni tali cliente occupa un filu sanu chì aspetta un avvenimentu. Iè, è avemu un ritardu supplementu di 1ms quandu l'avvenimentu hè attivatu, u più spessu questu ùn hè micca significativu, ma perchè fà u software peghju di ciò chì pò esse? Se sguassemu Thread.Sleep(1), allora in vanu carcheremu un core di processore 100% inattivu, rotendu in un ciculu inutile. Utilizendu TaskCompletionSource, pudete facilmente rimpruverà stu codice è risolve tutti i prublemi identificati sopra:

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

Stu codice ùn hè micca prontu per a produzzione, ma solu una demo. Per usà in i casi veri, avete ancu bisognu, à u minimu, di trattà a situazione quandu un missaghju ghjunghje à un mumentu chì nimu ùn l'aspittava: in questu casu, u metudu AsseptMessageAsync deve rinvià un Task digià cumpletu. Se questu hè u casu più cumuni, pudete pensà à utilizà ValueTask.

Quandu avemu ricevutu una dumanda per un missaghju, avemu criatu è postu un TaskCompletionSource in u dizziunariu, è dopu aspittà per ciò chì succede prima: l'intervallu di tempu specificatu scade o un messagiu hè ricevutu.

ValueTask: perchè è cumu

L'operatori async/wait, cum'è l'operatore di ritornu di rendiment, generanu una macchina statale da u metudu, è questu hè a creazione di un novu ughjettu, chì hè quasi sempre micca impurtante, ma in casi rari pò creà un prublema. Stu casu pò esse un metudu chì hè chjamatu veramente spessu, parlemu di decine è centinaie di millaie di chjama per seconda. Se un tali metudu hè scrittu in tale manera chì in a maiò parte di i casi torna un risultatu bypassing tutti i metudi d'aspittà, allura .NET furnisce un strumentu per ottimisà questu - a struttura ValueTask. Per esse chjaru, fighjemu un esempiu di u so usu: ci hè una cache chì andemu assai spessu. Ci sò qualchi valori in questu è dopu li vultemu solu; se no, andemu à qualchì IO lento per ottene. Vogliu fà l'ultime in modu asincronu, chì significa chì u metudu tutale hè asincronu. Cusì, a manera ovvia di scrive u metudu hè a siguenti:

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

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

A causa di u desideriu di ottimisà un pocu, è un pocu timore di ciò chì Roslyn genererà quandu compile stu codice, pudete scrivite stu esempiu cusì:

public Task<string> GetById(int id) {

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

Infatti, a suluzione ottima in questu casu seria di ottimisà u hot-path, vale à dì, ottene un valore da u dizziunariu senza alcuna allocazione inutile è carica nantu à u GC, mentre chì in quelli rari casi quandu avemu sempre bisognu à andà à IO per dati. , tuttu ferma un plus / minus u vechju modu:

public ValueTask<string> GetById(int id) {

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

Fighjemu un ochju più vicinu à stu pezzu di codice: se ci hè un valore in u cache, creamu una struttura, altrimente u veru compitu serà impannillatu in un significativu. U codice di chjamà ùn importa micca in quale strada hè stata eseguita stu codice: ValueTask, da un puntu di vista di sintassi C#, si cumportarà cum'è una Task regulare in questu casu.

TaskSchedulers: gestione di strategie di lanciu di attività

A prossima API chì mi piacerebbe cunsiderà hè a classe TaskScheduler è i so derivati. Aghju digià citatu sopra chì TPL hà a capacità di gestisce strategie per a distribuzione di Tasks in i fili. Tali strategie sò definite in i discendenti di a classa TaskScheduler. Quasi ogni strategia chì pudete bisognu pò esse truvata in a biblioteca. ParallelExtensionsExtras, sviluppatu da Microsoft, ma micca parte di .NET, ma furnitu cum'è un pacchettu Nuget. Fighjemu brevemente alcuni di elli:

  • Current ThreadTaskScheduler - esegue Tasks nantu à u filu attuale
  • LimitedConcurrencyLevelTaskScheduler - limita u numeru di Tasks eseguiti simultaneamente da u paràmetru N, chì hè accettatu in u custruttore
  • TaskScheduler urdinatu - hè definitu cum'è LimitedConcurrencyLevelTaskScheduler(1), cusì i travaglii seranu eseguiti in sequenza.
  • Work StealingTaskScheduler - attrezzi robba di travagliu approcciu à a distribuzione di u travagliu. Essenzialmente hè un ThreadPool separatu. Risolve u prublema chì in .NET ThreadPool hè una classa statica, una per tutte l'applicazioni, chì significa chì u so overloading o l'usu incorrectu in una parte di u prugramma pò purtà à effetti secundari in un altru. Inoltre, hè assai difficiule à capisce a causa di tali difetti. Chì. Pò esse bisognu di utilizà WorkStealingTaskSchedulers separati in parti di u prugramma induve l'usu di ThreadPool pò esse aggressivu è imprevisible.
  • QueuedTaskScheduler - permette di fà e so attività secondu e regule di fila di priorità
  • ThreadPerTaskScheduler - crea un filu separatu per ogni Task chì hè eseguitu nantu à questu. Pò esse utile per i travaglii chì piglianu un tempu imprevisible per finisce.

Ci hè un bonu detallatu un articulu circa TaskSchedulers nantu à u blog di Microsoft.

Per una debugging còmuda di tuttu ciò chì tocca à e Tasks, Visual Studio hà una finestra Tasks. In questa finestra pudete vede u statu attuale di u compitu è ​​saltà à a linea di codice attualmente in esecuzione.

.NET: Strumenti per travaglià cù multithreading è asincronia. Parte 1

PLinq è a classe Parallela

In più di Tasks è tuttu ciò chì dicenu nantu à elli, ci sò dui strumenti più interessanti in .NET: PLinq (Linq2Parallel) è a classa Parallel. U primu prumetti l'esekzione parallela di tutte l'operazioni Linq in parechje fili. U numaru di fili pò esse cunfigurati cù u metudu di estensione WithDegreeOfParallelism. Sfortunatamente, u più spessu PLinq in u so modu predeterminatu ùn hà micca abbastanza infurmazione nantu à l'internu di a vostra fonte di dati per furnisce un guadagnu di velocità significativu, invece, u costu di pruvà hè assai bassu: basta à chjamà u metudu AsParallel prima. a catena di i metudi Linq è eseguite teste di prestazione. Inoltre, hè pussibule di passà infurmazioni supplementari à PLinq nantu à a natura di a vostra fonte di dati utilizendu u mecanismu di Partizioni. Pudete leghje più ccà и ccà.

A classa statica Parallela furnisce metudi per iterazione attraversu una cullizzioni Foreach in parallelu, esecutà un loop For, è esecutà parechji delegati in parallel Invoke. L'esecuzione di u filu attuale serà fermata finu à chì i calculi sò finiti. U numaru di fili pò esse cunfigurati passendu ParallelOptions cum'è l'ultimu argumentu. Pudete ancu specificà TaskScheduler è CancellationToken usendu opzioni.

scuperti

Quandu aghju cuminciatu à scrive stu articulu basatu annantu à i materiali di u mo rapportu è l'infurmazioni chì aghju cullatu durante u mo travagliu dopu, ùn aghju micca aspittatu chì ci saria tantu. Avà, quandu l'editore di testu in quale scrivu stu articulu mi dice in modu reproche chì a pagina 15 hè andata, riassumeraghju i risultati interim. Altri trucchi, API, strumenti visuali è trappule seranu cuparti in u prossimu articulu.

Conclusioni:

  • Avete bisognu di sapè l'arnesi per travaglià cù filamenti, asincronia è parallelismu per utilizà e risorse di i PC muderni.
  • .NET hà assai strumenti diffirenti per questi scopi
  • Ùn sò micca tutti apparsu in una volta, cusì pudete truvà spessu i legacy, in ogni modu, ci sò manere di cunvertisce API antichi senza assai sforzu.
  • U travagliu cù i filamenti in .NET hè rapprisintatu da e classi Thread è ThreadPool
  • I metudi Thread.Abort, Thread.Interrupt è Win32 API TerminateThread sò periculosi è ùn sò micca cunsigliatu per l'usu. Invece, hè megliu aduprà u mecanismu CancellationToken
  • U flussu hè una risorsa preziosa è u so fornimentu hè limitatu. Situazioni induve i fili sò occupati in attesa di l'eventi deve esse evitata. Per questu hè cunvenutu à utilizà a classa TaskCompletionSource
  • L'uttene .NET più putenti è avanzati per travaglià cù parallelismu è asincronia sò Tasks.
  • L'operatori c# async/wait implementanu u cuncettu di attesa senza bloccu
  • Pudete cuntrullà a distribuzione di Tasks à traversu i fili usendu classi derivate da TaskScheduler
  • A struttura ValueTask pò esse utile per ottimisà i percorsi caldi è u trafficu di memoria
  • I Windows Tasks and Threads di Visual Studio furnisce assai informazioni utili per debugging codice multi-threaded o asincronu
  • PLinq hè un strumentu cool, ma ùn pò micca avè abbastanza infurmazione nantu à a vostra fonti di dati, ma questu pò esse riparatu cù u mecanismu di partitioning.
  • Per esse continuatu ...

Source: www.habr.com

Add a comment