Metode asincrone, async și await
Nu de puține ori programul execută astfel de operații, care pot dura mult timp, de exemplu, accesarea resurselor de rețea, citirea-scrierea fișierelor, accesarea bazei de date etc.
Astfel de operații pot încărca serios aplicația. Acest lucru este deosebit de relevant în aplicațiile grafice (desktop sau mobile), unde operațiile de lungă durată pot bloca interfața utilizatorului și influența negativ dorința utilizatorului de a lucra cu programul, sau în aplicațiile web, care trebuie să fie gata să deservească mii de cereri pe secundă.
Într-o aplicație sincronă, în timpul efectuării operațiilor de lungă durată în firul principal, acest fir pur și simplu ar fi blocat pe durata executării operației. Și pentru ca operațiile de lungă durată să nu blocheze funcționarea generală a aplicației, în C# se poate utiliza asincronitatea.
Asincronitatea permite mutarea unor sarcini separate din firul principal în metode asincrone speciale și, în același timp, utilizarea mai economică a firelor. Metodele asincrone se execută în fire separate.
Totuși, în timpul executării unei operații de lungă durată, firul metodei asincrone se întoarce în pool-ul de fire și va fi utilizat pentru alte sarcini. Iar când operația de lungă durată își încheie execuția, pentru metoda asincronă se alocă din nou un fir din pool-ul de fire, iar metoda asincronă își continuă activitatea.
Cheia pentru lucrul cu apelurile asincrone în C# sunt doi operatori: async și await, al căror scop este simplificarea scrierii codului asincron. Ei sunt folosiți împreună pentru a crea o metodă asincronă.
- În antetul metodei se utilizează modificatorul async
- Metoda conține una sau mai multe expresii await
- Ca tip de returnare se folosește unul dintre următoarele: void, Task, Task<T>, ValueTask<T>
O metodă asincronă, la fel ca una obișnuită, poate folosi un număr oricât de mare de parametri sau să nu folosească deloc. Totuși, o metodă asincronă nu poate defini parametri cu modificatorii out, ref și in.
De asemenea, trebuie menționat că cuvântul async, care se indică în definiția metodei, NU face automat metoda asincronă. El doar indică faptul că această metodă poate conține una sau mai multe expresii await.
Să analizăm un exemplu simplu de definire și apelare a unei metode asincrone:
await PrintAsync(); // apelul metodei asincrone
Console.WriteLine("Unele acțiuni în metoda Main");
void Print()
{
Thread.Sleep(3000); // imitația unei lucrări de lungă durată
Console.WriteLine("Hello FDC.COM");
}
// definirea metodei asincrone
async Task PrintAsync()
{
Console.WriteLine("Începutul metodei PrintAsync"); // se execută sincron
await Task.Run(() => Print()); // se execută asincron
Console.WriteLine("Sfârșitul metodei PrintAsync");
}
Aici, în primul rând, este definită o metodă obișnuită Print, care doar afișează un anumit șir pe consolă. Pentru imitația unei lucrări de lungă durată se folosește o întârziere de 3 secunde cu ajutorul metodei Thread.Sleep().
Adică, în mod condiționat, Print este o anumită metodă care execută o anumită operație de lungă durată. Într-o aplicație reală, aceasta ar putea fi accesarea bazei de date sau citirea-scrierea fișierelor, dar pentru simplificarea înțelegerii, ea doar afișează un șir pe consolă.
De asemenea, aici este definită o metodă asincronă PrintAsync(). Ea este asincronă deoarece are în definiție, înaintea tipului de returnare, modificatorul async, tipul ei de returnare este Task, și în corpul metodei este definită expresia await.
Trebuie menționat că metoda PrintAsync nu returnează explicit niciun obiect Task, dar deoarece în corpul metodei se aplică expresia await, ca tip de returnare se poate folosi tipul Task.
Operatorul await precede executarea sarcinii care va fi executată asincron. În acest caz, această operație reprezintă executarea metodei Print:
await Task.Run(()=>Print());
După regulile nescrise, în denumirea metodelor asincrone este obișnuit să se folosească sufixul Async - PrintAsync(), deși în principiu nu este obligatoriu.
Și apoi în program (în acest caz în metoda Main) este apelată această metodă asincronă.
await PrintAsync(); // apelul metodei asincrone
Să vedem ce va afișa programul pe consolă:
Începutul metodei PrintAsync
Hello FDC.COM
Sfârșitul metodei PrintAsync
Unele acțiuni în metoda Main
Să analizăm pe etape ce se întâmplă aici:
Se lansează programul, mai exact metoda Main, în care este apelată metoda asincronă PrintAsync.
Metoda PrintAsync începe să se execute sincron până la expresia await.
Console.WriteLine("Începutul metodei PrintAsync"); // se execută sincron
Expresia await lansează sarcina asincronă Task.Run(()=>Print())
În timp ce sarcina asincronă Task.Run(()=>Print()) se execută (și ea poate dura destul de mult timp), execuția codului se întoarce în metoda apelantă - adică în metoda Main.
Când sarcina asincronă își încheie execuția (în cazul de mai sus - afișează șirul după trei secunde), metoda asincronă PrintAsync, care a lansat sarcina asincronă, își continuă activitatea.
După încheierea metodei PrintAsync, își continuă activitatea metoda Main.
Metoda asincronă Main
Trebuie avut în vedere că operatorul await se poate aplica doar în metoda care are modificatorul async. Și dacă în metoda Main se folosește operatorul await, atunci metoda Main trebuie de asemenea să fie definită ca asincronă. Adică exemplul anterior va fi de fapt analog cu următorul:
class Program
{
async static Task Main(string[] args)
{
await PrintAsync(); // apelul metodei asincrone
Console.WriteLine("Unele acțiuni în metoda Main");
void Print()
{
Thread.Sleep(3000); // imitația unei lucrări de lungă durată
Console.WriteLine("Hello FDC.COM");
}
// definirea metodei asincrone
async Task PrintAsync()
{
Console.WriteLine("Începutul metodei PrintAsync"); // se execută sincron
await Task.Run(() => Print()); // se execută asincron
Console.WriteLine("Sfârșitul metodei PrintAsync");
}
}
}
Întârzierea operației asincrone și Task.Delay
În metodele asincrone pentru oprirea metodei pentru o anumită perioadă de timp se poate folosi metoda Task.Delay(). Ca parametru, aceasta primește numărul de milisecunde sub formă de valoare int sau obiectul TimeSpan, care stabilește timpul de întârziere:
await PrintAsync(); // apelul metodei asincrone
Console.WriteLine("Unele acțiuni în metoda Main");
// definirea metodei asincrone
async Task PrintAsync()
{
await Task.Delay(3000); // imitația unei lucrări de lungă durată
// sau așa
//await Task.Delay(TimeSpan.FromMilliseconds(3000));
Console.WriteLine("Hello FDC.COM");
}
Metoda Task.Delay reprezintă o operație asincronă, prin urmare operatorul await se aplică la aceasta.
Avantajele asincronității
Exemplele de mai sus sunt simplificări și probabil nu pot fi considerate exemplare. Să analizăm un alt exemplu:
PrintName("Tom");
PrintName("Bob");
PrintName("Sam");
void PrintName(string name)
{
Thread.Sleep(300
0); // imitația unei lucrări de lungă durată
Console.WriteLine(name);
}
Acest cod este sincron și execută secvențial trei apeluri ale metodei PrintName. Deoarece pentru imitația unei lucrări de lungă durată în metodă este stabilită o întârziere de trei secunde, execuția totală a programului va dura cel puțin 9 secunde.
Deoarece fiecare apel ulterior al metodei PrintName va aștepta finalizarea celui precedent.
Să modificăm în program metoda sincronă PrintName în una asincronă:
await PrintNameAsync("Tom");
await PrintNameAsync("Bob");
await PrintNameAsync("Sam");
// definirea metodei asincrone
async Task PrintNameAsync(string name)
{
await Task.Delay(3000); // imitația unei lucrări de lungă durată
Console.WriteLine(name);
}
În locul metodei PrintName, acum se apelează de trei ori PrintNameAsync. Pentru imitația unei lucrări de lungă durată în metodă este stabilită o întârziere de 3 secunde cu ajutorul apelului Task.Delay(3000).
Și deoarece la apelul fiecărei metode se folosește operatorul await, care oprește execuția până la finalizarea metodei asincrone, execuția totală a programului va dura din nou cel puțin 9 secunde. Totuși, acum execuția operațiilor asincrone nu blochează firul principal.
Acum să optimizăm programul:
var tomTask = PrintNameAsync("Tom");
var bobTask = PrintNameAsync("Bob");
var samTask = PrintNameAsync("Sam");
await tomTask;
await bobTask;
await samTask;
// definirea metodei asincrone
async Task PrintNameAsync(string name)
{
await Task.Delay(3000); // imitația unei lucrări de lungă durată
Console.WriteLine(name);
}
În acest caz, sarcinile sunt de fapt lansate la definire. Iar operatorul await se aplică doar atunci când trebuie să așteptăm finalizarea operațiilor asincrone - adică la sfârșitul programului. Și în acest caz, execuția totală a programului va dura cel puțin 3 secunde, dar mult mai puțin de 9 secunde.
Definirea unei expresii lambda asincrone
O operație asincronă poate fi definită nu doar cu ajutorul unei metode separate, ci și cu ajutorul unei expresii lambda:
// expresia lambda asincronă
Func<string, Task> printer = async (message) =>
{
await Task.Delay(1000);
Console.WriteLine(message);
};
await printer("Hello World");
await printer("Hello FDC.COM");