Internet Windows Android

Programare asincronă. Delegați asincroni Exemple de cod de programare asincronă c Ordonare

Asincronie în programare

Ivan Borisov

Programarea tradițională folosește programarea sincronă - execuția secvențială a instrucțiunilor cu apeluri de sistem sincrone care blochează complet firul de execuție până la finalizarea unei operațiuni de sistem, cum ar fi citirea de pe disc. Ca exemplu, serverul echo este scris mai jos:

While (adevărat) ( ​​std::string date; auto socket = Socket(localhost, port); socket.wait_connection(); while (!socket.end_of_connection()) (data = socket.read(); // Blocare socket(date // Blocare) )

Când metodele read() și write() sunt apelate, firul de execuție curent va fi întrerupt în timp ce se așteaptă I/O în rețea. Mai mult decât atât, de cele mai multe ori programul va aștepta pur și simplu. În sistemele foarte încărcate, asta se întâmplă cel mai adesea - aproape tot timpul programul așteaptă ceva: un disc, un DBMS, o rețea, o interfață de utilizare, în general, un eveniment extern independent de programul însuși. Pe sistemele puțin încărcate, acest lucru poate fi rezolvat prin crearea unui fir nou pentru fiecare acțiune de blocare. În timp ce un fir doarme, celălalt funcționează.

Dar ce să faci când sunt mulți utilizatori? Dacă creați cel puțin un fir de execuție pentru fiecare, performanța unui astfel de server va scădea brusc datorită faptului că contextul de execuție al firului de execuție se schimbă constant. De asemenea, fiecare thread are propriul context de execuție, inclusiv memoria stivă, care are o dimensiune minimă de 4 KB. Programarea asincronă poate rezolva această problemă.

Asincronie

Asincronia în programare este execuția unui proces într-un mod de apel de sistem neblocant, care permite firului de execuție a programului să continue procesarea. Există mai multe moduri de a implementa programarea asincronă, despre care veți afla mai jos.

Reapeluri

Pentru a scrie un program asincron, puteți utiliza funcții de apel invers (din engleză callback - callback) - funcții care vor fi apelate asincron de către un handler de evenimente după finalizarea sarcinii. Exemplu rescris de server folosind funcții de apel invers:

While (true) ( ​​​​auto socket = Socket(localhost, port); socket.wait_connection(); // Există încă un socket.async_read de blocare ((auto &data) /* * Firul nu este blocat, funcția lambda va fi apelat * de fiecare dată după primirea de date noi de la socket, * și firul principal va merge pentru a crea un nou socket și * așteaptă o nouă conexiune */ ( socket.async_write(data, (auto &socket) ( if (socket) .end_of_connection()) socket.close());

În wait_connection() mai așteptăm ceva, dar acum, în interiorul funcției wait_connection(), se poate implementa ceva de genul unui OS scheduler, dar cu funcții de callback (în timp ce așteptăm o nouă conexiune, de ce să nu le procesăm pe cele vechi ? De exemplu, prin coadă). Funcția de apel invers este apelată dacă pe socket au apărut date noi - lambda în async_read(), sau datele au fost scrise - lambda în async_write().

Ca rezultat, am obținut funcționarea asincronă a mai multor conexiuni într-un singur fir, care va aștepta mult mai rar. Această asincronie poate fi, de asemenea, paralelizată pentru a obține profit complet din utilizarea timpului CPU.

Există mai multe probleme cu această abordare. Primul se numește, în glumă, iadul callback. Este suficient sa cauti pe google poze pe acest subiect pentru a intelege cat de ilizibil si urat este. În exemplul nostru există doar două funcții de apel invers imbricate, dar pot fi multe altele.

A doua problemă este că codul nu mai arată sincron: există „sărituri” de la wait_connection() la lambdas, cum ar fi un lambda transmis la async_write() , care întrerupe secvența codului, făcând imposibil de prezis în ce ordine se numesc lambda. Acest lucru face codul dificil de citit și de înțeles.

Async/Așteptați

Să încercăm să facem cod asincron, astfel încât să pară sincron. Pentru o mai bună înțelegere, să schimbăm puțin sarcina: acum trebuie să citim datele din DBMS și un fișier folosind o cheie transmisă prin rețea și să trimitem rezultatul înapoi prin rețea.

Public async void work() ( var db_conn = Db_connection(localhost); var socket = Socket(localhost, port); socket.wait_connection(); var data = socket.async_read(); var db_data = db_conn.async_get(wait data) ; var file_data = Fișier(așteaptă date).async_read();

Să trecem prin program linie cu linie:

  • Cuvântul cheie asincron dintr-un antet de funcție îi spune compilatorului că funcția este asincronă și ar trebui să fie compilată diferit. Exact cum va face acest lucru este scris mai jos.
  • Primele trei linii ale funcției: crearea și așteptarea unei conexiuni.
  • Următoarea linie face citire asincronă fără a întrerupe firul principal de execuție.
  • Următoarele două linii fac o solicitare asincronă la baza de date și citesc fișierul. Operatorul de așteptare întrerupe funcția curentă până la finalizarea sarcinii asincrone de citire din baza de date și fișier.
  • Ultimele linii efectuează scriere asincronă pe socket, dar numai după ce am așteptat citirea asincronă din baza de date și fișier.

Acest lucru este mai rapid decât așteptarea secvenţială mai întâi pentru baza de date, apoi pentru fișier. În multe implementări, performanța async / await este mai bună decât cea a funcțiilor clasice de apel invers, în timp ce un astfel de cod este citit ca sincron.

Coroutine

Mecanismul descris mai sus se numește corutine. Puteți auzi adesea varianta „coroutine” (din engleză coroutine - coroutine).

Puncte de intrare multiple

În esență, corutinele sunt funcții care au mai multe puncte de intrare și ieșire. Funcțiile normale au un singur punct de intrare și mai multe puncte de ieșire. Dacă revenim la exemplul de mai sus, primul punct de intrare va fi apelul de funcție în sine folosind operatorul asincron, atunci funcția își va întrerupe execuția în loc să aștepte baza de date sau fișierul. Toate așteptările ulterioare nu vor reporni funcția, ci își vor continua execuția în punctul întreruperii anterioare. Da, în multe limbi pot exista mai multe așteptări într-o rutină.

Pentru o mai bună înțelegere, să ne uităm la codul din Python:

Def async_factorial(): rezultat = 1 în timp ce True: rezultat rezultat *= i fac = async_factorial() pentru i în interval (42): print(next(fac))

Programul va scoate întreaga secvență de numere factoriale cu numere de la 0 la 41.

Funcția async_factorial() va returna un obiect generator care poate fi transmis funcției next(), care va continua să execute coroutine până la următoarea instrucțiune yield, păstrând starea tuturor variabilelor funcției locale. Funcția next() returnează ceea ce trece declarația yield din interiorul corutinei. Deci, funcția async_factorial() are, în teorie, mai multe puncte de intrare și ieșire.

Stackful și Stackless

În funcție de utilizarea stivei, coroutinele sunt împărțite în stackful, unde fiecare coroutine are propriul său stivă, și stackless, unde toate variabilele funcției locale sunt stocate într-un obiect special.

Deoarece în coroutines putem pune o declarație yield oriunde, trebuie să stocăm undeva întregul context al funcției, care include cadrul de pe stivă (variabile locale) și alte meta-informații. Acest lucru se poate face, de exemplu, prin înlocuirea completă a stivei, așa cum se face în coroutine stivuite.

În figura de mai jos, apelul asincron creează un nou cadru de stivă și comută execuția firului la acesta. Acesta este practic un thread nou, doar că va fi executat asincron cu cel principal.

yield returnează, la rândul său, cadrul stivei precedente pentru execuție, stochând o referință la sfârșitul celui curent pe stiva anterioară.

A avea propriul stack vă permite să profitați de apelurile de funcții imbricate, dar astfel de apeluri sunt însoțite de o creare/modificare completă a contextului de execuție a programului, care este mai lent decât coroutinele fără stivă.

Mai productive, dar în același timp mai limitate, sunt corutinele fără stivuire. Ei nu folosesc stiva, iar compilatorul convertește o funcție care conține coroutine într-o mașină de stări fără coroutine. De exemplu, codul:

Def fib(): a = 0 b = 1 în timp ce Adevărat: randament a a += b randament b b += a

Va fi convertit în următorul pseudocod:

Class fib: def __init__(self): self.a = 0 self.b = 1 self.__rezultat: int self.__state = 0 def __next__(self): while True: if self.__state == 0: self.a = 0 self.b = 1 dacă self.__state == 0 sau self.__state == 3: self.__result = self.a self.__state = 1 return self.__result dacă self.__state == 1: self.a += self.b self.__result = self.b self.__state = 2 return self.__result if self.__state == 2: self.b += a self.__state = 3 break

În esență, aceasta creează o clasă care salvează toată starea funcției, precum și ultimul punct în care a fost apelat yield. Există o problemă cu această abordare: randamentul poate fi apelat numai în corpul unei funcții corutine, dar nu din cadrul funcțiilor imbricate.

Simetric și asimetric

Coroutinele sunt, de asemenea, împărțite în simetrice și asimetrice.

Simetric au un planificator global de coroutine, care selectează, dintre toate operațiunile asincrone în așteptare, pe cea care ar trebui să fie executată în continuare. Un exemplu este planificatorul discutat la începutul funcției wait_connection().

ÎN asimetric corutinele nu au un planificator global, iar programatorul, împreună cu suportul pentru compilator, alege ce corutine să execute și când. Majoritatea implementărilor de coroutine sunt asimetrice.

Concluzie

Programarea asincronă este un instrument foarte puternic pentru optimizarea programelor cu încărcare mare cu așteptări frecvente de sistem. Dar, ca orice tehnologie complexă, nu poate fi folosită doar pentru că există. Trebuie să vă puneți întotdeauna întrebarea: am nevoie de această tehnologie? Ce beneficii practice îmi va oferi? În caz contrar, dezvoltatorii riscă să cheltuiască mult efort, timp și bani fără a primi niciun profit.

Adnotare: Programare asincronă în .NET Framework. Metode EndOperation, Pooling, Callback. Lansarea asincronă a unei metode arbitrare. Actualizare interfață. Securitatea aplicațiilor cu mai multe fire. Sincronizare: automată, manuală; utilizarea zonelor de sincronizare. Control ProgressBar

Când rulăm o metodă „lungă”, procesorul o va executa, în timp ce „nu va avea timp” să îndeplinească alte solicitări ale utilizatorului (Figura 7.2).

Când rulează sincron, aplicația are un singur fir. Prin utilizarea model asincronÎn programare, puteți rula mai multe fire paralele și puteți reacționa la noile acțiuni ale utilizatorului în timp ce acestea rulează. Odată ce n-thread-ul este executat, afișați rezultatul pe ecran.

În viața de zi cu zi există multe exemple de comportament asincron - completarea cu materii prime într-un depozit permite fabricii să asigure funcționarea neîntreruptă. Un model nesincron ar fi utilizarea completă a materiilor prime și timpii de nefuncționare ulterioare în așteptarea livrării materialului.

Suport pentru programare asincronă în .NET Framework

Clasele care au suport încorporat model asincron, au o pereche de metode asincrone pentru fiecare dintre metodele sincrone. Aceste metode încep cu cuvintele Început și Sfârșit. De exemplu, dacă dorim să folosim versiunea asincronă a metodei Read a clasei System.IO.Stream, trebuie să folosim metodele BeginRead și EndRead din aceeași clasă.

Pentru a utiliza suportul încorporat model asincron programare, trebuie să apelați metoda corespunzătoare BeginOperation și să selectați modelul de finalizare a apelului. Apelarea metodei BeginOperation returnează un obiect de interfață IAsyncResult care determină starea de execuție a operației asincrone. Există mai multe moduri de a termina metodele asincrone.

Metoda EndOperation

Metoda EndOperation este utilizată pentru a încheia un apel asincron atunci când firul principal trebuie să efectueze o cantitate mare de calcul care este independent de rezultatele apelului de metodă asincronă. După ce munca principală este încheiată și aplicația are nevoie de rezultatele metodei asincrone pentru acțiuni ulterioare, se apelează metoda EndOperation. În acest caz, firul principal va fi suspendat până la finalizarea metodei asincrone. Un exemplu de utilizare a acestei metode:

Utilizarea sistemului; folosind System.IO; namespace EndOperation ( clasa Class1 ( static void Main(string args) ( // Creați un flux și deschideți fișierul. FileStream fs = new FileStream ("text.txt", FileMode.Open); byte fileBytes = new byte; // Run metoda Read într-un fir paralel.IAsyncResult ar = fs.BeginRead(fileBytes, 0, fileBytes.Length, null, null for(int i = 0; i.);<10000000; i++) { // Имитация длительной работы основного // потока, не зависящая от выполнения асинхронного метода. } // После завершения работы основного потока // запускаем завершение выполнения параллельного // метода Read. fs.EndRead(ar); Console.Write(System.Text.Encoding.Default.GetString(fileBytes)); } } } Листинг 7.1.

Pentru a încheia execuția firului paralel ar, aici a fost apelată metoda EndRead. Ca un cod care simulează munca pe termen lung, puteți conecta contorul exact de finalizare a sarcinilor, despre care am discutat în „Utilizarea bibliotecilor de cod în formularele Windows”.

Metoda sondajului

Această metodă este potrivită în cazurile în care trebuie să controlați execuția unei metode asincrone. Când îl utilizați, trebuie să verificați manual execuția metodei asincrone, folosind proprietatea IsCompleted a unui obiect de tip IAsyncResult. Aceasta nu este cea mai comună tehnică de terminare, deoarece majoritatea proceselor nu necesită control de execuție. Exemplu de utilizare a metodei de sondare:

Utilizarea sistemului; folosind System.IO; Namespace Pooling ( clasa Class1 ( static void Main(string args)) ( FileStream fs = new FileStream("text.txt", FileMode.Open); byte fileBytes = new byte; Console.Write ("Executați metoda Read asincron.") ; // Rulați metoda Read în mod asincron IAsyncResult ar = fs.BeginRead(fileBytes, 0, fileBytes.Length, null, null) // Verificați execuția metodei asincrone while(!ar.IsCompleted); citind fișierul. Ecranul afișează Console.Write("Proces în curs");

Cel mai probabil, nu veți observa aspectul inscripției în timpul procesului - fișierul va fi citit prea repede. Pentru a testa această metodă, scrieți un fișier text mare pe o dischetă, specificați adresa și încercați să rulați din nou aplicația.

Metoda de apel invers

Metoda Callback de a termina un apel asincron este utilizată în cazurile în care doriți să împiedicați blocarea firului principal. Când folosim Callback, rulăm metoda EndOperation în corpul metodei, care este apelată când metoda care rulează într-un fir paralel se termină. Semnătura metodei apelate trebuie să se potrivească cu semnătura delegatului AsyncCallback.

Un exemplu de utilizare a opțiunii de apel invers:

Utilizarea sistemului; folosind System.IO; namespace Callback ( clasa Class1 ( // Creați un flux și o matrice de octeți. static FileStream fs; static byte fileBytes; static void Main(string args) ( // Deschideți fișierul într-un flux. fs = new FileStream("text. txt", FileMode. Open); fileBytes = new byte; // Rulați metoda Read trecând asincron metoda WorkComplete fs.BeginRead(fileBytes, 0, (int)fs.Length, new AsyncCallback(WorkComplete), null ) // /

/// Metodă apelată automat când firul paralel se termină. /// /// Obiect de tip IAsyncResult. static void WorkComplete(IAsyncResult ar) ( // Începe sfârșitul metodei. fs.EndRead(ar); string textFromFile = System.Text.Encoding.Default.GetString(fileBytes); Console.Write(textFromFile); ) ) ) Lista 7.3.

Pe discul inclus cu cartea veți găsi aplicațiile EndOperation, Pooling și Callback (Code\Glava7\EndOperation, Pooling, Callback).

Ultima actualizare: 17.10.2018

Asynchrony vă permite să mutați sarcini individuale din firul principal în metode speciale asincrone sau blocuri de cod. Acest lucru este valabil mai ales în programele grafice, unde sarcinile lungi pot bloca interfața cu utilizatorul. Și pentru a preveni acest lucru, trebuie să utilizați asincronia. Asynchrony are beneficii și în aplicațiile web la procesarea cererilor de la utilizatori, la accesarea bazelor de date sau a resurselor de rețea. Pentru interogări mari la baza de date, metoda asincronă va adormi pur și simplu o perioadă până când primește date din baza de date, iar firul principal își poate continua munca. Într-o aplicație sincronă, dacă codul pentru primirea datelor a fost în firul principal, acest fir s-ar bloca pur și simplu în timpul primirii datelor.

Cheia pentru a lucra cu apeluri asincrone în C# sunt două cuvinte cheie: async și await, al căror scop este de a facilita scrierea codului asincron. Ele sunt folosite împreună pentru a crea o metodă asincronă.

Metoda asincronă are urmatoarele caracteristici:

    Antetul metodei folosește modificatorul asincron

    Metoda conține una sau mai multe expresii de așteptare

    Tipul de returnare este unul dintre următoarele:

    • ValueTask

O metodă asincronă, ca una obișnuită, poate folosi orice număr de parametri sau nu îi poate folosi deloc. Cu toate acestea, o metodă asincronă nu poate defini parametrii cu modificatorii out și ref.

De asemenea, este de remarcat faptul că cuvântul asincron, care este specificat în definiția unei metode, nu face automat metoda asincronă. Pur și simplu indică faptul că metoda poate conține una sau mai multe expresii de așteptare.

Să ne uităm la un exemplu de metodă asincronă:

Utilizarea sistemului; folosind System.Threading; folosind System.Threading.Tasks; namespace HelloApp ( clasa Program ( static void Factorial() ( int rezultat = 1; for(int i = 1; i)<= 6; i++) { result *= i; } Thread.Sleep(8000); Console.WriteLine($"Факториал равен {result}"); } // определение асинхронного метода static async void FactorialAsync() { Console.WriteLine("Начало метода FactorialAsync"); // выполняется синхронно await Task.Run(()=>Factorial()); // executat asincron Console.WriteLine("Sfârșitul metodei FactorialAsync"); ) static void Main(string args) ( FactorialAsync(); // apelarea metodei asincrone Console.WriteLine("Introduceți un număr: "); int n = Int32.Parse(Console.ReadLine()); Console.WriteLine($ „Numărul pătrat este egal cu (n * n)”;

Aici, în primul rând, este definită metoda obișnuită de calcul factorial. Pentru a simula timpi lungi de rulare, folosește o întârziere de 8 secunde folosind metoda Thread.Sleep(). În mod convențional, aceasta este o metodă care efectuează unele lucrări pe o perioadă lungă de timp. Dar pentru a fi mai ușor de înțeles, calculează pur și simplu factorialul de 6.

De asemenea, definește metoda asincronă FactorialAsync() . Este asincron deoarece are modificatorul asincron în fața tipului returnat în definiție, tipul său returnat este void, iar expresia await este definită în corpul metodei.

Expresia await specifică o sarcină care va fi executată asincron. În acest caz, o sarcină similară reprezintă implementarea funcției factoriale:

Așteptați Task.Run(()=>Factorial());

Conform regulilor nerostite, se obișnuiește să se folosească sufixul Async - FactorialAsync () în numele metodelor asincrone, deși în principiu acest lucru nu este necesar.

Obținem factorialul însuși în metoda asincronă FactorialAsync. Este asincron deoarece este declarat cu modificatorul asincron și conține utilizarea cuvântului cheie await.

Și în metoda Main numim această metodă asincronă.

Să vedem care va fi rezultatul consolei programului:

Începutul metodei FactorialAsync Introduceți un număr: 7 Pătratul numărului este 49 Sfârșitul metodei Main Factorial este 720 Sfârșitul metodei FactorialAsync

Să ne uităm la ce se întâmplă aici pas cu pas:

    Este rulată metoda Main, care apelează metoda FactorialAsync asincronă.

    Metoda FactorialAsync începe să se execute sincron până la expresia await.

    Expresia await rulează o sarcină asincronă Task.Run(()=>Factorial())

    În timp ce sarcina asincronă Task.Run(()=>Factorial()) rulează (și poate rula destul de mult timp), execuția codului revine la metoda de apelare - adică la metoda Main. În metoda Main, ni se va cere să introducem un număr pentru a calcula pătratul numărului.

    Acesta este avantajul metodelor asincrone - o sarcină asincronă care poate rula destul de mult timp nu blochează metoda Main și putem continua să lucrăm cu ea, de exemplu, introducerea și procesarea datelor.

    Când sarcina asincronă și-a încheiat execuția (în cazul de mai sus, a calculat factorialul unui număr), metoda FactorialAsync asincronă, care a numit sarcina asincronă, continuă să funcționeze.

Funcția factorială poate să nu fie cel mai bun exemplu, deoarece în realitate nu are rost să o facem asincronă în acest caz. Dar să ne uităm la un alt exemplu - citirea și scrierea unui fișier:

Utilizarea sistemului; folosind System.Threading; folosind System.Threading.Tasks; folosind System.IO; namespace HelloApp ( clasa Program ( static async void ReadWriteAsync() ( string s = "Bună lume! Un pas la un moment dat"); // hello.txt - fișierul care va fi scris și citit folosind (StreamWriter writer = new StreamWriter(" salut .txt", false)) ( așteaptă writer.WriteLineAsync(s); // scriere asincronă într-un fișier ) folosind (StreamReader reader = new StreamReader ("hello.txt")) ( șir rezultat = await reader.ReadToEndAsync() ; // citire asincronă din fișierul Console.WriteLine(rezultat) ) static void Main(string args) ( ReadWriteAsync(); Console.WriteLine("Câte lucru"); Console.Read(); ) )

Metoda asincronă ReadWriteAsync() scrie un șir într-un fișier și apoi citește fișierul scris. Astfel de operațiuni pot dura mult timp, mai ales cu cantități mari de date, așa că este mai bine să faceți astfel de operațiuni în mod asincron.

Cadrul .NET are deja suport încorporat pentru astfel de operațiuni. De exemplu, clasa StreamWriter definește o metodă WriteLineAsync(). De fapt, acesta reprezintă deja o operație asincronă și ia ca parametru un anumit șir care trebuie scris într-un fișier. Deoarece această metodă reprezintă o operație asincronă, putem încadra apelul la această metodă ca o expresie await:

Așteptați writer.WriteLineAsync(s); // scriere asincronă într-un fișier

În mod similar, clasa StreamReader definește o metodă ReadToEndAsync(), care reprezintă, de asemenea, o operație asincronă și care returnează tot textul citit.

Cadrul .NET Core definește multe metode similare. De regulă, acestea sunt asociate cu lucrul cu fișiere, trimiterea de solicitări de rețea sau interogări de baze de date. Sunt ușor de recunoscut după sufixul Async. Adică, dacă o metodă are un sufix similar în numele ei, atunci este mai probabil să fie folosită într-o expresie de așteptare.

Static void Main(șir argumente) ( ReadWriteAsync(); Console.WriteLine("Câte lucru"); Console.Read(); )

Din nou, când execuția în metoda ReadWriteAsync ajunge la prima expresie await, controlul revine la metoda Main și putem continua să lucrăm cu ea. Scrierea în fișier și citirea fișierului se vor face în paralel și nu vor bloca funcționarea metodei Main.

Definirea unei operații asincrone

După cum am menționat mai sus, cadrul .NET Core are multe metode încorporate care reprezintă o operație asincronă. Se termină cu sufixul Async. Și înainte de a apela astfel de metode, putem specifica operatorul await. De exemplu:

Scriitor StreamWriter = new StreamWriter("hello.txt", false); await writer.WriteLineAsync("Bună ziua"); // scriere asincronă într-un fișier

Sau putem defini noi înșine o operație asincronă folosind metoda Task.Run():

Static void Factorial() ( int rezultat = 1; for (int i = 1; i<= 6; i++) { result *= i; } Thread.Sleep(8000); Console.WriteLine($"Факториал равен {result}"); } // определение асинхронного метода static async void FactorialAsync() { await Task.Run(()=>Factorial()); // apelarea unei operații asincrone)

Puteți defini o operație asincronă folosind o expresie lambda:

Static async void FactorialAsync() ( așteaptă Task.Run(()) => ( int rezultat = 1; for (int i = 1; i<= 6; i++) { result *= i; } Thread.Sleep(8000); Console.WriteLine($"Факториал равен {result}"); }); }

Transmiterea parametrilor unei operații asincrone

Mai sus am calculat factorialul lui 6, dar să presupunem că vrem să calculăm factorialii diferitelor numere:

Utilizarea sistemului; folosind System.Threading; folosind System.Threading.Tasks; namespace HelloApp ( clasa Program ( static void Factorial(int n) ( int rezultat = 1; for (int i = 1; i)<= n; i++) { result *= i; } Thread.Sleep(5000); Console.WriteLine($"Факториал равен {result}"); } // определение асинхронного метода static async void FactorialAsync(int n) { await Task.Run(()=>Factorial(n)); ) static void Main(șir argumente) ( FactorialAsync(5); FactorialAsync(6); Console.WriteLine(„Câteva lucru”); Console.Read(); ) ) )

Obținerea rezultatului dintr-o operație asincronă

O operație asincronă poate returna un rezultat, pe care îl putem obține în același mod ca atunci când apelăm o metodă obișnuită:

Utilizarea sistemului; folosind System.Threading; folosind System.Threading.Tasks; namespace HelloApp ( clasa Program ( static int Factorial(int n) ( int rezultat = 1; for (int i = 1; i)<= n; i++) { result *= i; } return result; } // определение асинхронного метода static async void FactorialAsync(int n) { int x = await Task.Run(()=>Factorial(n)); Console.WriteLine($"Factorial este (x)"); ) static void Main(string args) ( FactorialAsync(5); FactorialAsync(6); Console.Read(); ) ) )

Metoda Factorial returnează o valoare de tip int, putem obține această valoare prin simpla atribuire a rezultatului unei operații asincrone unei variabile de acest tip: int x = await Task.Run(()=>Factorial(n));

De ceva timp am primit o mulțime de întrebări despre programarea asincronă. Și, realizând că acest subiect este de interes pentru mulți dintre cititorii mei, am decis să scriu un articol pentru a explica acești termeni, mai ales că Programarea asincronă este o parte foarte importantă a internetului modern.

Pentru început, trebuie remarcat faptul că există două concepte complet diferite: primul este modele de programare sincronă și asincronă, iar al doilea - medii cu un singur fir și cu mai multe fire. Fiecare dintre modelele de programare (sincron și asincron) poate funcționa atât în ​​medii cu un singur fir, cât și cu mai multe fire.

Model de programare sincronă.

În acest model de programare, un fir este alocat unei sarcini și începe să lucreze la el. Odată ce o sarcină este finalizată, firul este disponibil pentru următoarea sarcină. Acestea. o sarcină va fi înlocuită cu alta secvenţial. În acest model, nu este posibil să lăsați o sarcină la mijloc pentru a finaliza o altă sarcină. Să discutăm cum funcționează acest model în medii cu un singur și cu mai multe fire.

Mediu cu un singur thread- dacă avem câteva sarcini care trebuie executate și sistemul actual oferă un singur fir, atunci sarcinile sunt atribuite firului de execuție unul câte unul. Acest lucru poate fi reprezentat vizual astfel:

Unde Firma 1 - un fir, Sarcina 1 și Sarcina 2, Sarcina 3, Sarcina 4 – sarcini relevante.

Vedem că avem un flux ( Firma 1) Și patru sarcini care trebuie completate. Un fir începe să lucreze la sarcini și finalizează toate sarcinile una câte una.

Mediu cu mai multe fire - Multi-Fire- în acest mediu folosim mai multe fire care pot îndeplini aceste sarcini simultan. Asta înseamnă că avem bazin de fire(pot fi create fire noi, după cum este necesar, pe baza resurselor disponibile) și sarcini multiple.

Vedem că avem patru fire și același număr de sarcini. Prin urmare, fiecare fir îndeplinește o sarcină și o completează. Acesta este un scenariu ideal, dar în circumstanțe normale avem tendința de a avea mai multe sarcini decât numărul de fire disponibile. Și astfel, când un fir de execuție termină de executat o sarcină, va începe imediat să execute o alta. De asemenea, rețineți că un fir nou nu este creat de fiecare dată, deoarece are nevoie de resurse de sistem, cum ar fi ciclurile CPU și memoria, care ar putea să nu fie suficiente.

Acum să vorbim despre modelul asincron și despre modul în care se comportă într-un mediu cu un singur thread și cu mai multe fire.

Model de programare asincronă.

Spre deosebire de modelul de programare sincronă, aici un fir, care începe o anumită sarcină, poate opri execuția acesteia pentru o anumită perioadă de timp, menținându-și starea curentă, și poate începe executarea unei alte sarcini.

vedem că un fir este responsabil pentru executarea tuturor sarcinilor, alternându-le între ele.

Dacă sistemul nostru este capabil să creeze mai multe fire, atunci toate firele pot funcționa într-un model asincron.

Vedem că aceleași sarcini T4, T5, T6 procesate de mai multe fire. Aceasta este frumusețea și puterea acestui scenariu. După cum puteți vedea, sarcina T4 a fost lansat primul în thread Firma 1și completat în fir Firma 2. Similar T6 se termină la Firul 2, Firul 3 și Firul 4.

Deci, avem patru scenarii în total -

  • Un singur fir sincron
  • Sincron cu mai multe fire
  • Un singur fir asincron
  • Multi-threaded asincron

Beneficiile programării asincrone

Pentru orice aplicație, două lucruri sunt importante: gradul de utilizare și performanța. Utilizabilitatea este importantă, deoarece atunci când utilizatorul face clic pe un buton pentru a salva unele date, este nevoie de mai multe sarcini mici pentru a fi efectuate, cum ar fi citirea și popularea datelor într-un obiect intern, stabilirea unei conexiuni la serverul SQL și salvarea interogării acolo etc. . etc.

Deoarece SQL Server, de exemplu, rulează cel mai probabil pe un alt computer din rețea și rulează într-un proces diferit, acest lucru poate dura mult timp. Și, dacă aplicația rulează pe un singur fir, atunci ecranul dispozitivului utilizatorului va rămâne într-o stare inactivă până la finalizarea tuturor sarcinilor, ceea ce este un exemplu de interfață de utilizator foarte proastă. Acesta este motivul pentru care multe aplicații și noi cadre se bazează în întregime pe modelul asincron, deoarece vă permite să efectuați multe sarcini, menținând în același timp o interfață receptivă.

Eficiența aplicației este, de asemenea, foarte importantă. Se estimează că la executarea unei interogări se pierde aproximativ 70-80% din timp în așteptarea sarcinilor dependente. Prin urmare, aici este utilă programarea asincronă.

Astfel, în acest articol ne-am uitat la ce este programarea sincronă și asincronă. Un accent deosebit a fost pus pe programarea asincronă, deoarece aceasta stă la baza majorității instrumentelor moderne de dezvoltare. Și în articolele următoare ne vom familiariza cu exemple reale folosind modelul asincron.

Ultima actualizare: 31.10.2015

Subiectele anterioare au acoperit utilizarea asincroniei folosind cuvintele cheie asincrone și așteptare. Dar pe lângă acest model de utilizare a apelurilor asincrone în C#, există un alt model - utilizarea delegaților asincron. Delegații asincroni au fost utilizați pe scară largă înainte de introducerea asincronului și a așteptării în C#, dar acum asincronul și așteptarea fac scrierea codului asincron mult mai ușoară. Cu toate acestea, delegații asincroni pot fi încă utilizați. Deci haideți să le privim.

Delegații asincroni permit ca metodele către care indică delegații să fie apelate asincron. În subiectul despre delegați, s-a spus că delegații pot fi apelați fie folosind metoda Invoke, fie asincron folosind perechea de metode BeginInvoke/EndInvoke. Să ne uităm la un exemplu. Mai întâi, să vedem ce se întâmplă dacă folosim cod sincron obișnuit în aplicația noastră:

Utilizarea sistemului; folosind System.Threading; namespace AsyncApp ( clasa Program ( public delegate int DisplayHandler(); static void Main(string args) ( DisplayHandler handler = new DisplayHandler(Display); int rezultat = handler.Invoke(); Console.WriteLine("Metoda principală continuă"); Console.WriteLine(„Rezultatul este (0)”, rezultatul Console.ReadLine(); static int Display() (Console.WriteLine(„Metoda Display începe să funcționeze....”); ; pentru (int i = 1; i< 10; i++) { result += i * i; } Thread.Sleep(3000); Console.WriteLine("Завершается работа метода Display...."); return result; } } }

Acest lucru creează un delegat DisplayHandler special care ia ca referință o metodă fără parametri care returnează un număr. În acest caz, acea metodă este metoda Display, care funcționează. În acest caz, vom obține ceva de genul următor rezultat:

Metoda Display începe să funcționeze. Metoda Display continuă să funcționeze

În general, nu puteți folosi un delegat și nu puteți apela direct metoda Display. Dar, în orice caz, după apelarea acesteia, activitatea metodei Main este blocată în continuare până la finalizarea execuției metodei Display.

Acum să schimbăm exemplul folosind apeluri asincrone ale delegatului:

Utilizarea sistemului; folosind System.Threading; namespace AsyncApp ( clasa Program ( public delegat int DisplayHandler(); static void Main(string args) ( DisplayHandler handler = nou DisplayHandler(Display); IAsyncResult resultObj = handler.BeginInvoke(null, null); Console.WriteLine("Metoda continuă Main"); int rezultat = handler.EndInvoke(resultObj); Console.WriteLine("Rezultatul este (0)", rezultat); Console.ReadLine(); ) static int Display() ( Console.WriteLine("Metoda) începe să funcționeze Display...."); int rezultat = 0; for (int i = 1; i< 10; i++) { result += i * i; } Thread.Sleep(3000); Console.WriteLine("Завершается работа метода Display...."); return result; } } }

Esența acțiunilor a rămas practic neschimbată, aceeași metodă Display, doar că acum este apelată asincron folosind metodele BedinInvoke/EndInvoke. Și acum putem obține o ieșire ușor diferită:

Metoda Display începe să funcționeze. Metoda Display se încheie

Astfel, după apelarea metodei Display prin intermediul handler-ului de expresie.BeginInvoke(null, null), metoda Main nu este suspendată. Iar execuția metodei Display prin delegatul DisplayHandler are loc într-un alt thread. Și numai când execuția în metoda Main atinge linia int result = handler.EndInvoke(resultObj); se blochează și așteaptă ca metoda Display să finalizeze execuția.

Acum să ne uităm la caracteristicile utilizării metodelor BeginInvoke și EndInvoke și a interfeței IAsyncResult.