Sarcini și clasa Task
În era mașinilor multicore, care permit executarea simultană a mai multor procese, mijloacele standard de lucru cu thread-uri în .NET au devenit insuficiente. De aceea, în framework-ul .NET a fost adăugată biblioteca de sarcini paralele TPL (Task Parallel Library), al cărei funcțional principal se află în namespace-ul System.Threading.Tasks.
Această bibliotecă simplifică lucrul cu sistemele multiprocesor și multicore. În plus, simplifică lucrul de creare a noilor thread-uri. De aceea, se recomandă de obicei utilizarea TPL și a claselor sale pentru crearea aplicațiilor multithreading, deși mijloacele standard și clasa Thread sunt încă larg utilizate.
La baza bibliotecii TPL stă conceptul de sarcini, fiecare dintre acestea descriind o operație separată și continuă. În biblioteca de clase .NET, o sarcină este reprezentată de clasa Task, care se află în namespace-ul System.Threading.Tasks.
Această clasă descrie o sarcină separată, care se lansează asincron într-unul din thread-urile din pool-ul de thread-uri. Totuși, se poate lansa și sincron în thread-ul curent. În orice caz, trebuie menționat că o sarcină nu este un thread.
Pentru definirea și lansarea unei sarcini, există mai multe metode.
Prima metodă este crearea unui obiect Task și apelarea metodei Start:
Task task = new Task(() => Console.WriteLine("Hello Task!"));
task.Start();
Ca parametru, obiectul Task primește un delegat Action, deci putem transmite orice acțiune care corespunde acestui delegat, de exemplu, o expresie lambda, ca în acest caz, sau o referință la o metodă. În acest caz, la executarea sarcinii, pe consolă va fi afișat șirul "Hello Task!".
Metoda Start() lansează efectiv sarcina.
A doua metodă constă în utilizarea metodei statice Task.Factory.StartNew(). Această metodă, de asemenea, primește ca parametru un delegat Action, care specifică acțiunea ce va fi executată. Această metodă lansează imediat sarcina:
Task task = Task.Factory.StartNew(() => Console.WriteLine("Hello Task!"));
Ca rezultat, metoda returnează sarcina lansată.
A treia metodă de definire și lansare a sarcinilor este utilizarea metodei statice Task.Run():
Task task = Task.Run(() => Console.WriteLine("Hello Task!"));
Metoda Task.Run() poate primi ca parametru un delegat Action - acțiunea de executat și returnează un obiect Task.
Să definim un program mic în care folosim toate aceste metode:
Task task1 = new Task(() => Console.WriteLine("Task1 is executed"));
task1.Start();
Task task2 = Task.Factory.StartNew(() => Console.WriteLine("Task2 is executed"));
Task task3 = Task.Run(() => Console.WriteLine("Task3 is executed"));
În acest cod, sarcinile sunt create și lansate, dar la rularea aplicației pe consolă s-ar putea să nu vedem nimic. De ce? Deoarece atunci când thread-ul sarcinii este lansat din thread-ul principal al programului - thread-ul metodei Main, aplicația poate finaliza execuția înainte ca toate cele trei sarcini sau măcar una dintre ele să înceapă execuția. Pentru a preveni acest lucru, putem aștepta în mod programatic finalizarea sarcinii.
Așteptarea finalizării sarcinii
Pentru ca aplicația să aștepte finalizarea sarcinii, putem folosi metoda Wait() a obiectului Task:
Task task1 = new Task(() => Console.WriteLine("Task1 is executed"));
task1.Start();
Task task2 = Task.Factory.StartNew(() => Console.WriteLine("Task2 is executed"));
Task task3 = Task.Run(() => Console.WriteLine("Task3 is executed"));
task1.Wait(); // așteptăm finalizarea sarcinii task1
task2.Wait(); // așteptăm finalizarea sarcinii task2
task3.Wait(); // așteptăm finalizarea sarcinii task3
Posibilă ieșire pe consolă a programului:
Task3 is executed
Task2 is executed
Task1 is executed
Ieșirea pe consolă nu este deterministă, deoarece sarcinile nu se execută secvențial. Prima sarcină lansată poate finaliza execuția după ultima sarcină.
Trebuie menționat că metoda Wait() blochează thread-ul apelant, în care sarcina este lansată, până când această sarcină își finalizează execuția. De exemplu:
Console.WriteLine("Main Starts");
// creăm sarcina
Task task1 = new Task(() =>
{
Console.WriteLine("Task Starts");
Thread.Sleep(1000); // întârziere de 1 secundă - imitație a unei lucrări îndelungate
Console.WriteLine("Task Ends");
});
task1.Start(); // lansăm sarcina
task1.Wait(); // așteptăm finalizarea sarcinii
Console.WriteLine("Main Ends");
Pentru a emula o lucrare îndelungată, în sarcina task1 se setează o întârziere de 1 secundă. În final, când execuția ajunge la apelul task1.Wait(), thread-ul principal își va opri execuția și va aștepta finalizarea sarcinii. Și vom obține următoarea ieșire pe consolă:
Main Starts
Task Starts
Task Ends
Main Ends
Dacă acest comportament nu este esențial, așteptarea finalizării sarcinii poate fi plasată la sfârșitul metodei Main:
Console.WriteLine("Main Starts");
// creăm sarcina
Task task1 = new Task(() =>
{
Console.WriteLine("Task Starts");
Thread.Sleep(1000); // întârziere de 1 secundă - imitație a unei lucrări îndelungate
Console.WriteLine("Task Ends");
});
task1.Start(); // lansăm sarcina
Console.WriteLine("Main Ends");
task1.Wait(); // așteptăm finalizarea sarcinii
În acest caz, aplicația va aștepta totuși finalizarea sarcinii, dar alte acțiuni sincronizate în thread-ul principal nu vor fi blocate și nu vor aștepta finalizarea sarcinii.
Lansarea sincronă a sarcinii
În mod implicit, sarcinile sunt lansate asincron. Cu toate acestea, cu ajutorul metodei RunSynchronously() putem lansa sarcinile sincron:
console.WriteLine("Main Starts");
// creăm sarcina
Task task1 = new Task(() =>
{
Console.WriteLine("Task Starts");
Thread.Sleep(1000);
Console.WriteLine("Task Ends");
});
task1.RunSynchronously(); // lansăm sarcina sincron
Console.WriteLine("Main Ends"); // acest apel așteaptă finalizarea sarcinii task1
Proprietăți ale clasei Task
Clasa Task are o serie de proprietăți, prin intermediul cărora putem obține informații despre obiect. Unele dintre acestea sunt:
- AsyncState: returnează obiectul de stare al sarcinii
- CurrentId: returnează identificatorul sarcinii curente (proprietate statică)
- Id: returnează identificatorul sarcinii curente
- Exception: returnează obiectul excepției apărute în timpul execuției sarcinii
- Status: returnează starea sarcinii
System.Threading.Tasks.TaskStatus, care are următoarele valori:
- Canceled: sarcina a fost anulată
- Created: sarcina a fost creată, dar nu a fost lansată încă
- Faulted: în timpul execuției sarcinii a apărut o excepție
- RanToCompletion: sarcina a fost finalizată cu succes
- Running: sarcina este lansată, dar nu este finalizată încă
- WaitingForActivation: sarcina așteaptă activarea și planificarea execuției
- WaitingForChildrenToComplete: sarcina a fost finalizată și așteaptă finalizarea sarcinilor copil atașate
- WaitingToRun: sarcina este planificată pentru execuție, dar nu a început încă execuția
- IsCompleted: returnează true dacă sarcina este finalizată
- IsCanceled: returnează true dacă sarcina a fost anulată
- IsFaulted: returnează true dacă sarcina s-a încheiat cu apariția unei excepții
- IsCompletedSuccessfully: returnează true dacă sarcina s-a încheiat cu succes
Folosim unele dintre aceste proprietăți:
Task task1 = new Task(() =>
{
Console.WriteLine($"Task{Task.CurrentId} Starts");
Thread.Sleep(1000);
Console.WriteLine($"Task{Task.CurrentId} Ends");
});
task1.Start(); // lansăm sarcina
// obținem informații despre sarcină
Console.WriteLine($"task1 Id: {task1.Id}");
Console.WriteLine($"task1 is Completed: {task1.IsCompleted}");
Console.WriteLine($"task1 Status: {task1.Status}");
task1.Wait(); // așteptăm finalizarea sarcinii
Exemplu de ieșire pe consolă:
task1 Id: 1
Task1 Starts
task1 is Completed: False
task1 Status: Running
Task1 Ends