Lucrul cu clasa Task
Sarcini imbricate
O sarcină poate lansa o altă sarcină imbricată. Aceste sarcini se execută independent una de cealaltă. De exemplu:
var outer = Task.Factory.StartNew(() => // sarcină exterioară
{
Console.WriteLine("Outer task starting...");
var inner = Task.Factory.StartNew(() => // sarcină imbricată
{
Console.WriteLine("Inner task starting...");
Thread.Sleep(2000);
Console.WriteLine("Inner task finished.");
});
});
outer.Wait(); // așteptăm finalizarea sarcinii exterioare
Console.WriteLine("End of Main");
Deși aici așteptăm finalizarea sarcinii exterioare, sarcina imbricată poate finaliza execuția chiar și după încheierea metodei Main:
Outer task starting...
End of Main
Sarcina internă poate chiar să nu înceapă execuția până la terminarea thread-ului principal al programului. În acest caz, sarcinile exterioare și imbricate se execută independent una de cealaltă.
Dacă este necesar ca sarcina imbricată să se execute ca parte a sarcinii exterioare, trebuie utilizată valoarea TaskCreationOptions.AttachedToParent:
var outer = Task.Factory.StartNew(() => // sarcină exterioară
{
Console.WriteLine("Outer task starting...");
var inner = Task.Factory.StartNew(() => // sarcină imbricată
{
Console.WriteLine("Inner task starting...");
Thread.Sleep(2000);
Console.WriteLine("Inner task finished.");
}, TaskCreationOptions.AttachedToParent);
});
outer.Wait(); // așteptăm finalizarea sarcinii exterioare
Console.WriteLine("End of Main");
Ieșire pe consolă:
Outer task starting...
Inner task starting...
Inner task finished.
End of Main
În acest caz, sarcina imbricată este atașată sarcinii exterioare și se execută ca parte a acesteia. Sarcina exterioară se va încheia doar atunci când toate sarcinile imbricate atașate se vor încheia.
Array de sarcini
La fel ca și în cazul thread-urilor, putem crea și lansa un array de sarcini. Putem defini toate sarcinile într-un array direct prin intermediul obiectului Task:
Task[] tasks1 = new Task[3]
{
new Task(() => Console.WriteLine("First Task")),
new Task(() => Console.WriteLine("Second Task")),
new Task(() => Console.WriteLine("Third Task"))
};
// lansăm sarcinile din array
foreach (var t in tasks1)
t.Start();
Sau putem folosi metodele Task.Factory.StartNew sau Task.Run pentru a lansa imediat toate sarcinile:
Task[] tasks2 = new Task[3];
int j = 1;
for (int i = 0; i < tasks2.Length; i++)
tasks2[i] = Task.Factory.StartNew(() => Console.WriteLine($"Task {j++}"));
În ambele cazuri, putem întâlni situația în care toate sarcinile din array se pot finaliza după ce metoda Main, care lansează aceste sarcini, își termină execuția:
Task[] tasks = new Task[3];
for(var i = 0; i < tasks.Length; i++)
{
tasks[i] = new Task(() =>
{
Thread.Sleep(1000); // emulare de lucru îndelungat
Console.WriteLine($"Task{i} finished");
});
tasks[i].Start(); // lansăm sarcina
}
Console.WriteLine("Finalizarea metodei Main");
Posibilă ieșire pe consolă:
Finalizarea metodei Main
Dacă este necesar ca programul să finalizeze execuția sau să execute un anumit cod doar după ce toate sarcinile din array se finalizează, putem folosi metoda Task.WaitAll(tasks):
Task[] tasks = new Task[3];
for(var i = 0; i < tasks.Length; i++)
{
tasks[i] = new Task(() =>
{
Thread.Sleep(1000); // emulare de lucru îndelungat
Console.WriteLine($"Task{i} finished");
});
tasks[i].Start(); // lansăm sarcina
}
Console.WriteLine("Finalizarea metodei Main");
Task.WaitAll(tasks); // așteptăm finalizarea tuturor sarcinilor
În acest caz, mai întâi se vor finaliza toate sarcinile, și abia după aceea se va executa codul ulterior din metoda Main:
Finalizarea metodei Main
Task0 finished
Task1 finished
Task2 finished
Totuși, ordinea de execuție a sarcinilor din array nu este deterministă.
De asemenea, putem folosi metoda Task.WaitAny(tasks). Aceasta așteaptă finalizarea oricărei sarcini din array.
Returnarea rezultatelor din sarcini
Sarcinile nu doar se execută ca proceduri, ci pot returna și anumite rezultate:
int n1 = 4, n2 = 5;
Task<int> sumTask = new Task<int>(() => Sum(n1, n2));
sumTask.Start();
int result = sumTask.Result;
Console.WriteLine($"{n1} + {n2} = {result}"); // 4 + 5 = 9
int Sum(int a, int b) => a + b;
În primul rând, pentru a obține un rezultat dintr-o sarcină, trebuie să tipizăm obiectul Task cu tipul obiectului pe care dorim să-l obținem din sarcină. De exemplu, în exemplul de mai sus, așteptăm să obținem un număr de tip int din sarcina sumTask, astfel că tipizăm obiectul Task cu acest tip - Task<int>.
În al doilea rând, sarcina trebuie să execute o metodă care returnează acest tip de obiect. În acest caz, sarcina execută metoda Sum, care primește două numere și returnează suma lor - o valoare de tip int.
Rezultatul returnat va fi stocat în proprietatea Result: sumTask.Result. Nu trebuie să-l convertim la tipul int, deoarece va reprezenta deja un număr.
int result = sumTask.Result;
Atunci când accesăm proprietatea Result, thread-ul curent își oprește execuția și așteaptă obținerea rezultatului din sarcina executată.
Un alt exemplu:
Task<Person> defaultPersonTask = new Task<Person>(() => new Person("Tom", 37));
defaultPersonTask.Start();
Person defaultPerson = defaultPersonTask.Result;
Console.WriteLine($"{defaultPerson.Name} - {defaultPerson.Age}"); // Tom - 37
record class Person(string Name, int Age);
În acest caz, sarcina defaultPersonTask returnează un obiect de tip Person, pe care îl putem obține din proprietatea Result.