Sincronizarea thread-urilor
Deseori, în thread-uri sunt utilizate anumite resurse partajate, comune pentru întreaga programă. Acestea pot fi variabile comune, fișiere, alte resurse. De exemplu:
int x = 0;
// pornim cinci thread-uri
for (int i = 1; i < 6; i++)
{
Thread myThread = new(Print);
myThread.Name = $"Thread {i}"; // setăm numele pentru fiecare thread
myThread.Start();
}
void Print()
{
x = 1;
for (int i = 1; i < 6; i++)
{
Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
x++;
Thread.Sleep(100);
}
}
Aici pornim cinci thread-uri care apelează metoda Print și care lucrează cu variabila comună x. Ne așteptăm ca metoda să afișeze toate valorile x de la 1 la 5 pentru fiecare thread. Totuși, în realitate, în timpul execuției, va avea loc comutarea între thread-uri, iar valoarea variabilei x devine imprevizibilă. De exemplu, în cazul meu, am obținut următoarea ieșire pe consolă (poate varia în funcție de fiecare caz):
Thread 1: 1
Thread 5: 1
Thread 4: 1
Thread 2: 1
Thread 3: 1
Thread 1: 6
Thread 5: 7
Thread 3: 7
Thread 2: 7
Thread 4: 9
Thread 1: 11
Thread 4: 11
Thread 2: 11
Thread 3: 14
Thread 5: 11
Thread 1: 16
Thread 2: 16
Thread 3: 16
Thread 5: 18
Thread 4: 16
Thread 1: 21
Thread 5: 21
Thread 3: 21
Thread 2: 21
Thread 4: 21
Soluția problemei constă în sincronizarea thread-urilor și restricționarea accesului la resursele partajate în timpul utilizării acestora de către un thread. Pentru aceasta, se folosește cuvântul cheie lock. Operatorul lock definește un bloc de cod, în interiorul căruia tot codul este blocat și devine inaccesibil pentru alte thread-uri până la finalizarea execuției thread-ului curent.
Celelalte thread-uri sunt puse într-o coadă de așteptare și așteaptă până când thread-ul curent eliberează acest bloc de cod. Astfel, cu ajutorul lock, putem modifica exemplul anterior astfel:
int x = 0;
object locker = new(); // obiect placeholder
// pornim cinci thread-uri
for (int i = 1; i < 6; i++)
{
Thread myThread = new(Print);
myThread.Name = $"Thread {i}";
myThread.Start();
}
void Print()
{
lock (locker)
{
x = 1;
for (int i = 1; i < 6; i++)
{
Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
x++;
Thread.Sleep(100);
}
}
}
Pentru blocare, cuvântul cheie lock folosește un obiect placeholder, în acest caz, variabila locker. De obicei, aceasta este o variabilă de tip object. Când execuția ajunge la operatorul lock, obiectul locker este blocat, și pe durata blocării, accesul exclusiv la blocul de cod este asigurat doar pentru un singur thread.
După finalizarea execuției blocului de cod, obiectul locker este eliberat și devine accesibil pentru alte thread-uri.
În acest caz, ieșirea pe consolă va fi mai ordonată:
Thread 1: 1
Thread 1: 2
Thread 1: 3
Thread 1: 4
Thread 1: 5
Thread 5: 1
Thread 5: 2
Thread 5: 3
Thread 5: 4
Thread 5: 5
Thread 3: 1
Thread 3: 2
Thread 3: 3
Thread 3: 4
Thread 3: 5
Thread 2: 1
Thread 2: 2
Thread 2: 3
Thread 2: 4
Thread 2: 5
Thread 4: 1
Thread 4: 2
Thread 4: 3
Thread 4: 4
Thread 4: 5