Sari la conținutul principal

Clasa Task

In general se prefera sa nu partajati date decat daca e absolut necesar, pentru majoritatea aplicatiilor nu se foloseste modelul de date partajate si in general se prefera un stil de programare asincron si functional unde fiecare unitate de lucru (task) se realizeaza doar cu date proprii in cadrul unui thread. In multe aplicatii datele care sunt partajate in mare parte provin din alte aplicatii care sunt optimizate pentru procesarea paralela si concurenta cum sunt de altfel si bazele de date care se asigura de consistenta (chiar si in cele din urma) a datelor. Din acest motiv, multe aplicatii multi-threaded sunt gandite sa folosesca cat mai putin modelul de date partajate in special pentru a simplifica logica aplicatiei, a marii calitatea codului si reduce probabilitatea aparitiei erorilor.

In C# exista peste thread-uri o alta abstractizare mai utila in multe situatii care implementeaza idea de promise sau future (desi se folosesc in general ca fiind acelasi termen sunt diferite concepte, practic un promise este un obiect care la un moment dat i se poate atribui o valoare care poate fi asteptata si citita iar un future este doar un obiect care la un moment dat va returna o valoare).

Cea mai simpla metoda de a crea un task nou este cu Task.Run(Action) pentru un Task sau Task.Run(Func<T>) pentru un Task<T> care si porneste procesarea pe un thread separat, task-ul poate fi asteptat cu metoda Wait() si daca este un Task<T> rezultatul poate fi citit din proprietatea Result.

O alta posibilitate este sa folosim o functie asincrona, o functie asincrona este o functie care are pus cuvantul cheie async si returneaza un Task (poate fi si void dar nu se recomanda). Diferenta intre o functie async si una normala este ca doar in interiorul acesteia se poate folosi cuvantul cheie await pentru a astepta terminarea altui task si eventual returnarea valorilor aceluia. In plus, cand se face return in functia async nu se face return la tipul Task sau Task<T> cum e in signatura ci nu returneaza nicmic, respectiv returneaza o instanta T.

// Cream o clasa care sa execute operatiile pentru task-uri.
public class TaskWork
{
public int WorkLoad { get; }

public TaskWork(int workLoad)
{
WorkLoad = workLoad;
}

public int Execute()
{
var result = 0;

for (var i = 0; i < WorkLoad; ++i)
{
++result;

// Simulam aici o computatie internsiva printr-un sleep.
Thread.Sleep(10);
}

Console.WriteLine("Thread {0} finished work!", Thread.CurrentThread.Name);

return result;
}

// Putem avea o functie async care sa returneze un Task cu un CancellationToken pentru oprire.
public async Task<int> ExecuteAsync(CancellationToken cancellation = default)
{
var result = 0;

for (var i = 0; i < WorkLoad; ++i)
{
// Daca se cere oprirea acestui task returnam sau aruncam exceptie.
if (cancellation.IsCancellationRequested)
{
Console.WriteLine("Task was canceled!");

return result;
}

++result;

// Putem apela alta functie asincrona si sa asteptam un task intr-o functie async, putem pasa si tokenul.
await Task.Delay(10, cancellation);
}

Console.WriteLine("Thread {0} finished work!", Thread.CurrentThread.Name);

return result;
}
}

private static void RunTaskExample()
{
var work = new TaskWork(Workload);
Console.WriteLine("Starting the worker task!");
var sw = Stopwatch.StartNew();
// Putem crea un task nou cu Task.Run.
var task = Task.Run(() => work.Execute());
Console.WriteLine("Waiting for the worker task!");

// Aici terminarea task-ului este asteptata.
task.Wait();

sw.Stop();

// Rezultatul il putem citi in Result.
Console.WriteLine("Work executed async in {0} milliseconds with result: {1}", sw.ElapsedMilliseconds, task.Result);
Console.WriteLine("---------------");
}

private static void RunTaskAsyncExample()
{
var work = new TaskWork(Workload);
Console.WriteLine("Starting the worker task async!");
var sw = Stopwatch.StartNew();

// Orice functie async poate returna un task care poate fi asteptat.
var task = Task.Run(() => work.ExecuteAsync());
Console.WriteLine("Waiting for the worker task!");

task.Wait();

sw.Stop();

Console.WriteLine("Work executed in {0} milliseconds with result: {1}", sw.ElapsedMilliseconds, task.Result);
Console.WriteLine("---------------");
}

private static void RunTaskAsyncWithCancelExample()
{
var work = new TaskWork(Workload);
Console.WriteLine("Starting the worker task async!");
// Cu sursa pentru un CancellationToken putem opri executia.
var cancellationTokenSource = new CancellationTokenSource();

var sw = Stopwatch.StartNew();
// Daca vrem sa oprim task-ul pasam de la sursa un CancellationToken.
var task = Task.Run(() => work.ExecuteAsync(cancellationTokenSource.Token), cancellationTokenSource.Token);
Console.WriteLine("Waiting for the worker task!");

// Putem folosi metoda Cancel sau CancelAfter pentru a opri executia.
cancellationTokenSource.CancelAfter(2000);

try
{
// La wait daca e pasat token-ul si se opreste inainte de finalizare primi exceptie.
task.Wait(cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
Console.Error.WriteLine("The task was canceled!");
}

sw.Stop();

// Putem sa vedem si daca un task s-a executat pana la capat sau a fost oprit inainte.
Console.WriteLine("Work canceled after {0} milliseconds with status: {1}", sw.ElapsedMilliseconds, task.Status);
Console.WriteLine("---------------");
}

Clasa Task este usor de folosit iar aplicatiile, fie ca vorbim de aplicatii de web sau de desktop, prefera sa foloseasca aceasta abstractizare peste fire de executie deoarece deja incapsuleaza operatiile care trebuie sa se execute pe fire separate cu metode de a returna rezultatele.

Mai mult decat atat, folosind cuvintele cheie async si await este foarte usor de a crea task-uri si faciliteaza scierea de cod asincron intr-un mod transparent, usor de urmarit dar si eficient. Task-ul este pus la rulare intr-un pool de thread-uri de catre framework ca sa fie preluat de un thread liber, cand task-ul executa await acesta se suspenda, thread-ul pe care ruleaza va prelua alt task (depinde de cum functioneaza scheduler-ul framework-ului), si task-ul nou o sa fie la un moment dat executat si la terminare se revine la task-ul initial suspendat, nu neaparat pe acelasi thread de pe care a pornit.

Chiar daca programarea asincrona nu este un aspect a programarii orientate pe obiecte, este facilitata de aceasta si in multe alte limbaje cum sunt JavaScript/Typescript sau Kotlin au suport pentru aceasta paradigma si este foarte util de stiut.

Jeton de anulare

Un lucru util la folosirea task-urilor este ca pot fi oprite/anulate folosind un jeton de anulare (cancelation token). De obicei veti observa ca in majoritatea bibliotecilor din C# acolo unde sunt metode async aveti ca ultim parametru optional un obiect de tip CancellationToken. Este o structura prin care se pot anula mai multe task-uri daca acestea trebuie oprite fortat sau daca unul vrea sa semnaleze terminarea unei procesari prin partajarea acestuia si folosirea metodelor de anulare cu sunt Cancel si CancelAfter.

Jetonul se creeaza printr-un CancellationTokenSource care are proprietatea Token, apoi cand se anuleaza task-urile se poate verifica proprietatea IsCancellationRequested a jetonului ca sa se stie daca sa se opreasca din procesare sau nu. Cand se anuleaza, in multe situatii se arunca exceptia OperationCanceledException care va semnala ca cineva a cerut anularea procesarii.

Ca practica buna, mereu cand implementati metoda async adaugati ca ultim parametru si un CancellationToken cu valoarea default iar in metoda fie verificati daca a fost ceruta anularea, ca de exemplu intr-o bucla, fie il pasati mai departe la alte metode async apelate.