Sari la conținutul principal

Clasa Thread

Cea mai simpla metoda de a crea un thread este sa folosim clasa Thread. Aceasta ia ca argument in constructor o functie fara parametri sau o functie cu un parametru object? si care nu returneaza nimic. Dupa crearea unei instante pentru Thread trebuie apelate doua metode, prima fiind Start() care acesta porneste efectiv firul de executie si apeleaza functia din acel fir in paralel cu executia firului curent de executie. Dupa lansarea thread-ului, fluxul de instructiuni paralel trebuie unificat inapoi in cel principal folosind metoda Join() din thread-ul care l-a creat, thread-ul v-a astepta terminarea celuilalt thread si il va inchide. Astfel putem crea thread-uri ca in exemplul urmator, folositi functii lamda ca sa transmiteti date si sa returnati date prin obiecte.

/* Folosim aceasta clasa sa incapsulam datele care sunt trimise catre Thread
* si rezultatele in urma procesarii, asa putem partaja date la mai fire de
* executie si sa returnam rezultatele.
*/
public class Work
{
// Aici putem adauga datele, partajate sau proprii thread-urlui si rezultate.
public int WorkLoad { get; }
public int Result { get; private set; }

public Work(int workLoad)
{
WorkLoad = workLoad;
Result = 0;
}

// Putem sa includem si ce procesare vream sa executam asupra dateleor.
public void Execute()
{
for (var i = 0; i < WorkLoad; ++i)
{
/* Facem o procesare, in mod intentionat variavila aceasta
* este scrisa in mod neprotejat pentru a demonstra cum se poate face acces exclusiv pe ea.
* Daca obiectul curent este partajat de mai multe thread-uri scrierea in memorie nu va fi
* protejata si cand un thread o citeste altul o poate scrie si va fi actualizata apoi
* valoarea anterioara ducand la un rezultat gresit in final.
* Aceasta zona unde se scrie si citeste in mod paralel se numeste zona critica.
*/
++Result;

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

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

// Cream si o varianta thread-safe a executie pentru a vedea diferente la rezultat.
public void SafeExecute()
{
for (var i = 0; i < WorkLoad; ++i)
{
/* Folosim cuvantul cheie lock pentru a face acces exclusiv pe zona critica.
* Aici doar un thread poate intra la un moment dat in aceasta zona si
* se foloseste de referinta la un obiect, in acest caz este this pentru a
* face acces exclusiv pe intanta acestei clase daca este folosita de muai
* multe thread-uri.
*/
lock (this)
{
++Result;
}

Thread.Sleep(10);
}

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

private static void RunSingleThreadExample()
{
var work = new Work(Workload);
Console.WriteLine("Starting the worker on main thread!");
var sw = Stopwatch.StartNew();
// Putem ca referinta sa rulam si pe thread-ul principal.
work.Execute();
sw.Stop();

Console.WriteLine("Work executed (on main thread) in {0} milliseconds with result: {1}", sw.ElapsedMilliseconds, work.Result);
Console.WriteLine("---------------");
work = new(Workload);
// Cream un thread cu paramtru functia care trebuie executata pe acel thread.
var workThread = new Thread(() => work.Execute())
{
// Putem adauga si un nume pentru identificarea thread-ului.
Name = "Second Thread"
};
Console.WriteLine("Starting the worker thread!");
sw.Restart();

// Dupa creere ca sa ruleze thread-ul nou se apeleaza Start().
workThread.Start();

Console.WriteLine("Waiting for the worker thread!");

/* Orice thread trebuie inchis cu join.
* Thread-ul curent ramane intr-o stare de asteptare pana
*/
workThread.Join();
sw.Stop();

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

private static void RunUnsafeThreadsExample()
{
// Putem de asemenea sa impartim lucrul la mai multe thread-uri
var sharedWork = new Work(Workload / NumberOfThreads);
var threads = Enumerable.Range(0, NumberOfThreads).Select(i =>
new Thread(() => sharedWork.Execute())
{
Name = $"Thread {i}"
}).ToList();

Console.WriteLine("Starting the worker threads!");
var sw = Stopwatch.StartNew();

foreach (var thread in threads)
{
thread.Start();
}

Console.WriteLine("Waiting for the worker threads!");

foreach (var thread in threads)
{
thread.Join();
}

sw.Stop();

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

private static void RunSafeThreadsExample()
{
// Mai executam inca o daca ca referinta pentru rezultate varianta thread-safe a functiei de executie.
var sharedWork = new Work(Workload / NumberOfThreads);
var threads = Enumerable.Range(0, NumberOfThreads).Select(i =>
new Thread(() => sharedWork.SafeExecute())
{
Name = $"Thread {i}"
}).ToList();

Console.WriteLine("Starting the worker threads!");
var sw = Stopwatch.StartNew();

foreach (var thread in threads)
{
thread.Start();
}

Console.WriteLine("Waiting for the worker threads!");

foreach (var thread in threads)
{
thread.Join();
}

sw.Stop();

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

Detalii de folosire pentru clasa Thread

Trebuie sa fiti atenti cand folositi thread-uri, in principal exista doua riscuri daca aceasta clasa este folosita gresit si este valabil si in alte limbaje.

In primul rand, daca un thread iese fortat din executie cum este cu functia Environment.Exit() sau cand o exceptie nu este prinsa nicaieri se inchide tot programul. Aceste situatii trebuie preintampinate, cel mai bine este sa inconjurati cu try-catch ce doriti sa executati pe thread si sa tratati toate cazurile de exceptie.

In al doilea rand, trebuie mentionat ca este un mit faptul ca nu exista memory-leaking in limbaje cu garbage collection. In acest caz, daca unui thread nu i se face join, acesta va ramane in memoria programului si orice referinta pe care o detine nu va fi eliberata, astfel resurse nu vor fi eliberate pana la join. Trebuie facut join la toate firerele de executie care au fost pornite pentru a elibera resurse, in special memorie. De asemenea la programe de lunga durata acestea la un moment dat nu vor mai putea deschide thread-uri.

Acestea sunt aspecte legate in general de thread-uri. Altfel, problematic pentru programatori este lucrul cu datele partajate. Daca se face doar citire pe o zona de memorie partajata nu este o problema pentru cazul multi-threaded, insa daca se pot intampla scrieri in acelasi timp pot aparea inconsistente pentru ca un thread poate citi o valoare si altul sa scrie in memorie o valoare veche iar rezultatele finale ale unei executii pot fi atat gresite cat si nedeterministe, adica la repetarea rularii apar alte rezultate.

Zonele de cod unde se screie si citest in mod paralel date partajate se numesc zone critice. Din acest motiv, exista mecanisme de sincronizare care fac ca thread-urile sa aiba acces exclusiv pe aceste zone critice. In C# pentru a avea acces exclusiv intr-o zona de memorie se foloseste cuvantul cheie lock pe o variabila referinta ca in exemplu, cand se face lock pe acea variabila referinta un thread detine acea referinta si are acces exclusiv pe acea zona critica de cod, doar acel thread poate sa execute zona respectiva pana iese din scope si face unlock, adica cedeaza referinta sa o preia alt thread.