Documente Academic
Documente Profesional
Documente Cultură
2
3
Un thread (fir de executie) este o modalitate de a impartii un
proces in mai multe bucati ce ruleaza simultan (bucati numite
task-uri).
Thread-uri multiple pot fi executate in paralel pe acelasi
calculator sau pe calculatoare diferite.
Multithreading-ul se poate face in 2 feluri:
folosind Time slice-uri. Procesorul executa un thread o perioada de
timp si apoi trece la executia altui thread. Aici apare iluzia unui
multitasking.
pe sistemele multiprocesor. Procesoul are mai multe core-uri sau
sistemul este multiprocesor.
Sistemele de operare noi pot implementa ambele tipuri de
multithreading simultan prin intermediul unui scheduler.
4
Scheduler-ul poate avea doua modalitati de a alege ce thread se
executa la un moment dat:
preemptive: Sistemul de operare determina ce schimbare de context
(ce thread) trebuie sa aleaga in continuare.
cooperative: Thredurile singure renunta la executie
Threadurile in un proces impart spatiul de memorie.
In unele situatii apare o distinctie intre threadurile kernel-level si
user-level. Primele sunt organizate si programate pentru rulare
de catre kernel. Urmatoarele sunt organizate de catre user.
Programarea threadurile user-level in interiorul procesului este
facuta de catre utilizator.
Kernelul sistemului de operare permite manipularea
threadurilor prin apel de functii sistem.
5
6
Concluzie:
Un proces este cea mai “mare” unitate a unui scheduler
multitasking. Procesele detin resurse sistem (memorie, file
handle-uri, device handle-uri, ferestre windows, etc.). Procesele
NU impart spatiul de memorie (si handle-urile) cu alte procese
decat prin apeluri explicite de functii. Procesele in general sunt
programate pentru executie preemptiv.
Un thread este ce mai “mica” unitate a unui scheduler
multitasking. Cel putin un thread exista in un proces. Daca exista
mai multe threaduri in proces acestea impart spatiul de
memorie si handle-urile. Thread-urile sunt executate preemptiv.
Thread-urile nu detin resurse cu exceptia unei stive si a unei copii
a registrilor.
7
In C# vom folosii pentru a lucra cu thread-uri clasa Thread.
Proprietati:
IsAlive: precizeaza daca threadul este in executie sau nu
IsBackground: precizeaza daca threadul este rulat in background sau
nu
IsThreadPoolThread: prezieaza daca threadul este in thread pool
Name: numele thread-ului
Priority: prioritate
ManagedThreadId: Un id unic alocat de sistem pentru thread
ThreadState: Aborted, AbordRequested, Background, Running,
Stopped, StopRequested, Suspended, SuspendedRequested,
Unstarted, WaitSleepJoin
8
Metode:
Abort: Arunca o exceptie thread-ului
(ThreadAbordException) pentru a-i indica ca trebuie sa se
opreasca
Interrupt: Arunca o exceptie thread-ului
(ThreadInterruptException) cand acesta este in o stare de
asteptare (WaitSleepJoin).
Join: Blocheaza thread-ul apelant pana cand thread-ul care
are metoda join se termina.
Resume/Suspend: a nu se folosii
Start: porneste thread-ul
9
Metode si proprietati statice:
Proprietati:
CurrentContext: intoarce un obiect de tip ThreadContext
asociat thread-ului ce ruleaza acum
CurrentPrincipal: user-ul detinator al thread-ului ce
ruleaza acum
CurrentThread: tread-ul ce ruleaza acum
Metode:
BeginCriticalRegion, EndCriticalRegion, Sleep,
ResetAbort, etc.
10
1> Cream o metoda statica fara argumente si
care nu intoarce nici o valoare.
2> Cream un delegat ThreadStart si il legam la
metoda de mai sus.
3> Cream un obiect Thread ce primeste ca
parametru obiectul ThreadStart
4> Apelam metoda Thread.Start
11
static void SimpleWork()
{
Console.WriteLine(“Thread {0}”,
Thread.CurrentThread.ManagedThreadId;
}
threadul.Start();
12
threadul.Join();
Apelarea metodei Join opreste aplicatia noasta sa astepte oprirea
thread-ului.
Ex:
Thread[] threaduri = new Thread[5];
for (int i=0;i<5;i++)
{
threaduri[i] = new Thread(operatie);
thread[i].Start();
}
for (i=0;i<5;i++)
threaduri[i].Join();
Acest exemplu creaza 5 threaduri si apoi asteapta terminarea
fiecaruia.
13
Putem influenta modul in care schedulerul programeaza
thread-urile modificat prioritatea acestora.
Pentru a modifica prioritatea unui thread modificam
prioritatea Priority una din valorile din enumerarea
ThreadPriority:
HighPriority
AboveNormal
Normal
BelowNormal
Lowest
In general nu veti creste performanta unui thread prin
cresterea prioritatii acestuia.
14
In exemplele de mai sus am trimis la Thread un
delegat ce pointa la o metoda fara nici un
parametru.
Daca dorim sa apelam o metoda cu parametru vom
folosii un nou delegat numit
ParameterizedThreadStart.
Noua metoda va avea un parametru un obiect de tip
object.
Noul delegat va fi trimis ca parametru la crearea
thread-ului (exact ca la ThreadStart simplu).
15
static void CuParametru(object obj)
{
string info = (string) obj;
Console.WriteLine(obj + “ Thread “
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(10);
}
threadul.Start(“Salutari”);
16
Mecanismul princiapal de oprire a unui thread
este apelarea metodei .Abort();
Cand este apelata aceasta metoda systemul de
threading arunca in thread o exceptie de tip
ThreadAbortException.
Indiferent daca exceptia este prinsa sau nu
thread-ul este oprit.
17
static void AcestThread()
{
OClasa.IsValid=true;
OClasa.IsComplete=true;
OClasa.WriteToConsole();
}
18
static void AcestThread()
{
Thread.BeginCriticalRegion();
OClasa.IsValid=true;
OClasa.IsComplete=true;
Thread.EndCriticalRegion();
OClasa.WriteToConsole();
}
Pentru a rezolva problema vom crea o regiune critica timp in care threadul nu
poate fi oprit.
Daca am primit o exceptie ThreadAbort cat suntem in interiorul regiunii critice
aceasta exceptie este ignorata pana cand termina regiunea critica.
Intrarea in regiunea critica se face cu Thread.BeginCriticalRegion(); si iesirea
cu Thread.EndCriticalRegion();
19
20
Fiecare thread are asociat un context de
executie.
Acesta include:
Informatii de securitate (proprietarul thread-ului si
identitatea acestuia)
Setarile de localizare (cultura in care ruleaza threadul)
Informatii de transactie (din System.Transactions)
Vom discuta despre toate acestea in capitolele
urmatoare.
21
In mod normal ExectuionContext “curge” spre
thread-urile create de aplicatia noastra
Pentru a creste performanta putem oprii “curgerea”:
ExecutionContext.SuppressFlow();
ExecutionContext.RestoreFlow();
Puteti folosii ExecutionContext si pentru a rula cod
arbitrar in un thread.
Vom folosii metoda Run si metoda
ExecututionContext.Capture;
22
ExecutionContext ctx =
ExecutionContext.Capture();
ExecututionContext.Run(ctx, new
ContextCallback(ContextCalled), null);
23
Cea mai mare problema ce apare atunci cand
lucram cu threaduri este impartirea de date.
Aceasi data poate sa fie accesata (si modificat)
simultan de 2 sau mai multe threaduri, cauzand
inconsistente.
24
public class Counter
{
public static int count;
}
25
ThreadStart starter = new ThreadStart(UpdateCount);
Thread[] threaduri = new Thread[10];
Counter.count = 0;
for (int i=0;i<10;i++)
{
thread[i] = new Thread(starter);
thread[i].Start();
}
Console.WriteLine(Counter.count);
26
Daca rulati acest cod pe o calculator singlecore si
fara hyperthreading sunt sanse ca rezultatul sa fie
corect.
Daca in schimb rulati o masina
multicore/hyperthreading rezultatul va fi in general
gresit.
Problema apare deoarece operatia ++ nu este
atomica.
27
Cum putem face operatia ++ atomica?: Folosind
clasa Interlocked.
Clasa interlocked are metodele Add, Decrement,
Exchange, Increment, Read, toate operatii
atomica.
ex: in locul liniei Counter.count++ vom scrie linie
Interlocked.Increment(ref Counter.count);
28
Rezultatul va fi corect.
Problema este ca nu putem folosii clasa
interlocked decat pentru un numar mic de tipuri
de date.
29
public class Counter
{
public static int count;
public static int countPar;
}
Cand rulam acest cod count-ul simplu va f corect, dar count-ul de numere pare va fi uneori gresit.
Asta se intampla deoarece 2 threaduri pot venii si updata countul simplu unul dupa altul iar verificarea
Counter.count%2 sa piarda un increment.
30
Pentru a rezolva aceasta problema C# are syncronization locks (reprezentate de cuvantul cheie lock).
Metoda UpdateCount va devenii:
31
Un syncronization lock blocheaza accesul altor
threadurila la zona din lock atat timp cand un
thread este acolo.
Lock este cea mai simpla metoda in c# de a
sincromiza 2 threaduri.
O metoda mai complexa dar mai versatila este
folosirea unui monitor (reprezentat in c# de clasa
monitor);
32
static void UpdateCount()
{
Monitor.Enter(ana);
try
{
for (int i = 0; i < 10000; ++i)
{
Interlocked.Increment(ref Counter.count);
if (Counter.count % 2 == 0)
Interlocked.Increment(ref Counter.countPar);
}
}
finally
{
Monitor.Exit(ana);
}
}
Clasa monitor are 4 metode: Enter, Exit, TryEnter, Wait
Enter: Creaza un lock exclusiv pe obiectul respectiv
Exit: Elibereaza lockul exclusiv
TryEnter: Incearca sa creeze un loc exclusiv. Poate sa aiba ca parametru un timp de timeout.
Wait: Elibeaza lock-ul exclusiv dar blocheaza threadul actual pana acesta reprimeste lock-ul exlusiv
33
Deadlock (interblocaj) este cazul cand 2 bucati de cod incearca sa acceseze un obiect dar se
blocheaza una pe cealalata.
Avem deadlock in urmatorul caz:
class Deadlocker
{
Deadlocker deadlock = new Deadlocker();
object ResursaA = new Object();
ThreadStart firstStart = new ThreadStart(deadlock.Second);
object ResursaB = new Object();
ThreadStart secondStart = new ThreadStart(deadlock. Second);
public void First()
Thread first = new Thread(firstStart);
{
Thread second = new Thread(secondStart);
lock (ResursaA)
first.Start();
{
second.Start();
lock(ResursaB)
first.Join();
{
second.Join();
Console.WriteLine(“First”);
}
Deadlock-ul apare in urmatorul fel:
}
1> Primul thread porneste si blockeaza ResursaA
}
2> Al doilea thread porneste si blockeaza ResursaB
public void Second()
3> Primul thread se blocheaza cerand ResursaB care e blocata
{
4> Al doilea thread se blocheaza cerand ResursaA
lock (ResursaB)
5> Aplicatia decedeaza.
{
lock(ResursaA)
{
Console.WriteLine(“Second”);
}
}
}
} 34
C# nu ofera o moditalitate exacta de verificare
de existenta deadlock
Totusi puteti folosii metoda Monitor.TryEnter si
un timeout.
Pentru mai multe informatii vedeti cursul de SO
anul 3 Calculatoare.
35
Pe langa monitor in c# mai sunt disponibile
urmatoarele metode de sincronizare
clasa ReaderWriterLock
obiecte kernel Windows
clasa Mutex
clasa Semaphore
clasa AutoResetEvent
clasa ManualResetEvent
36
Exista resurse ce pot fi scrise de un singur scriitor dar
pot fi citite de mai multi simultan (dar cand scriitorul
scrie niic un cititor nu poate citii)
Clasa ReaderWriterLock ofera o modalitate de a
diferentia intre cititori si scriitori la un obiect.
Proprietati:
IsReaderLockHeld> returneaza un indicator ce ne spune
daca exista lock reader
IsWriterLockHeld > returneaza un indicator ce ne spune
daca exista lock writer
37
Metode:
AcquireReaderLock: creaza un lock de reader. Metoda primeste
ca parametru un timp de timeout in care va fi cerut lock-ul. Daca
nu este primit in acest timp este aruncata o exceptie
AcquireWriterLock: creaza un lock de writer. Metoda primeste
ca parametru un timp de timeout in care va fi cerut lock-ul. Daca
nu este primit in acest timp este aruncata o exceptie
DowngradeFromWriterLock: writer lock devine reader lock
UpgradeToWriterLock: reader lock devine writer lock
ReleaseReaderLock: elibereaza reader lock
ReleaseWriterLock: elibereaza writer lock
38
ReaderWriterLock rwLock = new ReaderWriterLock(); ReaderWriterLock rwLock = new ReaderWriterLock();
int counter=0; int counter=0;
try try
{ {
rwLock.AcquireReaderLock(100); rwLock.AcquireWriterLock(1000);
try try
{ {
Console.WriteLine(counter); Interlock.Incerement(ref counter);
} }
finally finally
{ {
rwLock.ReleaseReaderLock(); rwLock.ReleaseWriterLock();
} }
} }
catch (ApplicationException) catch (ApplicationException)
{ {
Console.WriteLine(“nu am primit readerlock”); Console.WriteLine(“nu am primit writerlock”);
} }
39
Daca dorim ca un cititor sa devina temporar scriitor (sau invers) vom folosii
clasa LockCookie:
try
{
LockCookie cookie = rwLock.UpgradeToWriterLock(1200);
counter++;
rwLock.DowngradeFromWriterLock(cookie);
{
catch (ApplicationException)
{
//nu am putut devenii scriitor deci nu scriem
}
40
La nivelul sistemului de operare exista 3 obiecte kernel
(Mutex, Semaphore, Event) care au rolul de a ajuta in
sincronizarea intre threaduri.
Desi aceastea oferta facilitati puternice ele au viteza
mica.Ex: clasa Mutex mere de appox 33 de ori mai incet
decat lock.
Avantajul lor principal este ca putem sinconiza threaduri
peste granita de process sau AppDomain-uri (mai clar
putem sincroniza acces-ul intre procese).
Fiecare obiect kernel are o clasa in c#. (Mutex, Semaphore,
AutoResetEvent si ManualResetEvent). Toate acestea
deriva din clasa WaitHandle.
41
Mutex: Permite sincronizarea de tip lock peste
granita de AppDomain si proces
Semaphore: este folosit pentru a restrictiona
accesul la o resursa protejata in functie de
numarul de threaduri ce o acceseaza
Event: folosit pentru a notifica threaduri din mai
multe AppDomain-uri sau procese ca un
eveniment a avut loc
42
Clasa Mutex functioneaza exact ca un lock simplu, numai ca un astfel de lock
este vizibil in toate procesele din sistem si in toate AppDomain-urile din
procese.
Folosire Mutex:
1> Se creaza o instanta a clasei Mutex:
Mutex m = new Mutex();
2>In interiorul unui thread creati un if care apeleaza metoda WaitOne din mutex-ul creat,
pana cand lock-ul asupra obiectului devine disponibil.
if (m.WaitOne(1000,false)) //asteapta o secunda
{ }
3> puneti un try-finally in acest if iar in finaly eliberati mutex-ul
m.ReleaseMutex();
4> optional creati un else la acest if in care sa tratati cazul in care nu s-a primit lock-ul.
Puteti folosi metoda OpenExisting pentru a deschide un Mutex deja existent.
Aceasta metoda arunca o exceptie daca nu a fost gasit Mutex-ul.
43
Un semafor functioneaza foarte asemanator cu un
mutex.
Singura diferenta este ca el permite accesul mai multor
thread-uri la resursa.
Un semafor are un numar fix de sloturi in care pot intra
thread-urile.
Daca un thread gaseste un slot liber va putea accesa
resursa.
Daca un thread nu a primit accesul se blocheaza in o
coada. Fiecare semafor are o coada proprie de thread-uri
blocate.
44
Creare semafor:
Semaphore semafor = new Semaphore(0,10);
primul parametru este numarul initial de sloturi
ocupate si al doilea numarul maxim de sloturi ce pot
fi ocupate
Eliberare resurse:
semafor.Release(5); // numarul de sloturi pe care il
elibereaza.
Puteti folosii metoda OpenExisting ca si la Mutex.
45
Un event este un obiect kernel care poate avea 2 valori: ON si
OFF.
Cand se modifica starea toate threadurile din sistem pot
observa ca aceasta stare s-a modificat.
Sunt 2 tipuri de event-rui: AutoResetEvent si
ManualReserEvent.
AutoResetEvent este similar cu Mutex-ul. In momentul in care
un event a devenit ON primul thread care observa schimbarea il
trece pe OFF.
ManualResetEvent este similar cu Semaphore-ul. In momentul
in care eventul devine ON toate threadurile care asteapta
schimbarea vor detecta ON-ul si nu vor trece eventul pe OFF.
Ambele clase mostenesc clasa EventWaitHandle.
46
AutoResetEvent are = new AutoResetEvent(true);
ManualResetEvent mre = new
ManualResetEvent(true);
are.Set();
mre.Reset();
47
Ca orice obiect kernel level si acestea sunt disponibile la toate thread-urile din sistem.
Suportul pentru nume este disponibil in clasa EventWaitHandle (nu in AutoResetEvent si nici in
ManualResetEvent);
ex:
EventWaitHandle event = null;
try
{
event = EventWaitHandle.OpenExisting(“THEEVENT”);
}
catch (WaitHandleCannotBeOpenedException)
{
//nu exista event-ul
}
48
Modelul de programare asincron permite unor
portiuni de cod sa fie executate in thread-uri
separate, fara a fie nevoie de crearea explicita a
unui thread nou.
Multe clase in .NET freamework au metode cu
numele .Begin**** si .End****. Ex: FileStream.
49
byte [] buffer = new byte[100];
///APM
int numBytes = fs.EndResult(result);
fs.Close();
50
APM-ul este reprezentat de metodele BeginRead si
EndRead
Begin read seamana mult cu o functie Read normala:
void Read(byte[] array, int offset, int count);
voir BeginRead(byte[] array, int offset, int numBytes,
AsyncCallback callback, object stateObject);
Metoda EndRead opreste operatia asincrona. Cand
apelati metoda EndRead cu parametrul IAsyncResult
aceasta va returna octetii cititi.
51
Exista 3 modalitati de a trata momentul in care se termina
citirea:
Wait-Until-Done
Polling
Callback
Wait-Until-Done: Dupa ce ati ponit operatia asincrona cu
metoda Begin**** executati ce operatii vreti simultan cu
executarea metodei. Dupa ce ati terminat operatiile executati
metoda End*****. Daca operatia asincrona nu s-a terminat veti
astepta pana aceasta se termina si rezultatul il veti primii ca
valoare returnata de EndRead.
Asa functioneaza exemplul anterior
52
In momentul in care apelam BeginRead parametrii callback si
state object sunt null.
Pooling:Modelul pooling este similar, cu exceptia ca vom
verifica periodic in IAsyncResult daca operatia s-a terminat, deci
nu ne vom bloca la End*****.
while (!result.IsCompleted)
{
//executam o actiune cat timp nu s-a terminat operatia
asincrona
}
Proprietatea IsCompleted in obiectul IAsyncResult ne spune
daca s-a terminat sau nu operatia asincrona
53
Callback: Pentru a folosii modelul callback este nevoie sa specificam ca
parametru a functia Begin***** o metoda ce va fi apelata la finalul operatiei
asincrone. Aceasta metoda va primii obiectul stateObject
54
Exceptiile in APM pot fi tratate la apelarea
metodei End*****.
55
.NET framework contine un mecanism automat de creare si
management al threadurilor, numit ThreadPool
Tot ce trebuie sa faceti pentru a folosi ThreadPool-ul este sa
apelati metoda QueueWorkItem.
Aceasta primeste ca parametru un obiect de tip WaitCallback,
un delegat ce pointeaza la o metoda cu un singur parametru de
tip object si care nu intoace nici o valoare.
ex:
WaitCallback workItem = new WaitCallback(CuParametru);
if (!ThreadPool.QueueWorkItem(workItem, “ThreadPooled”);
Console.WriteLine(“NU am introdus thread in threadpool”);
56
Folosirea unui ThreadPool este mai rapida decat creare manuala a
threadurilor, deoarece threadurile pot fi refolosite.
Cum un thread devine disponibil ThreadPool-ul ii da un nou work item.
Un ThreadPool are urmatoarele metode disponibile (printre altele):
GetAvailableThreads
GetMaxThreads
GetMinThreads
QueueUserWorkItem
RegisterWaitForSingleObject: Permite apelarea unui callback pentru un
anumit WaitHandle cand acel WaitHandle este semnalizat.
SetMaxThreads
SetMinThreads
57
In general nu este nevoie sa limitati numarul de threaduri
in un ThreadPool.
Pot sa apara 2 cazuri in care doriti sa schimbati setarile
implicite:
Infometarea: Aplicatia voastra foloseste un ThreadPool dar
acesta functioneaza incet pentru ca are prea multe work items in
lucru. Pentru a reduce numarul de work items pe care il accepta
vom schimba numarul maxim de threaduri acceptate prin apelul
metodei SetMaxThreads
Al doilea caz este acela cand timpul de pornire a unui thread este
prea mare. Crescand numarul minim de threaduri ce sunt
gestionate de ThreadPool va creste numarul de threaduri nu vor
astepta terminarea unui operatii aflata deja in executie
58
Metodele GetMax,GetMin, SetMax si SetMin
Thread primesc sau intorc un parametru de tip
completion port.
Un completion port este un thread kernel-level
folosit pentru operatii de I/O cu fisiere
59
Cum am vazut mai devreme exista obiecte la nivel
de nucleu pe care le putem folosi pentru
sincronizarea si schimbul de date intre threaduri
(Mutex, Semaphore, Event), si ca acestea toate
mostenesc clasa WaitHandle.
ThreadPool permite si el folosirea obiectelor de tip
WaitHandle pentru sincronizarea threadurilor.
In momentul in care se face o schimbare in Mutex,
Semaphore, Event va fi declansat un callback.
60
Inregistrea callback-ului ce va fi declansat se face prin metoda RegisterWaitForSingleObject
ex:
ThreadPool.RegisterWaitForSingleObject (mutex, new WaitOrTimerCallback(MutexHasFired), null,
TimeOut.Infinite, true);
mutex.ReleaseMutex(); // apelam aceasta metoda pentru a declasa threadul.
61
Modelul de thread in Windows Forms este diferit
de cel in ASP.NET. In timp ce Windows Forms
prefera ca majoritatea operatiilor sa aiba loc in
threadul main user interface, ASP.NET foloseste
un ThreadPool.
Pentru a putea scrie cod optim indiferent de
platorma pentru care dezolvati veti folosi un
SyncronizationContext.
62
SyncronizationContext are 2 metode: Send si Post.
Metoda Send porneste in executie o metoda cu un
parametru care nu intoare nici o valoare si asteapta
terminarea acesteia. Send poate sa ruleze metoda in
threadul curent sau in alt thread.
Metoda Post porneste executia unei metode de
acelasi tip da nu blocheaza executia daca rularea se
va face pe alt thread. Altfel executia este blocata.
63
SyncronizationContext ctx =
SyncronizationContext.Current;
ctx.Send(RunMe, “Hi”);
cts.Post(RunMe,”Hi”);
64
Un timer este un obiect care lanseaza asincron metode
in executie la intervale de timp.
Cand creati un timer specificati un delegat TimerCallback
care pointeaza la o metoda ce se va rula cand valoarea
trece perioada de timp precizata timerului.
In plus puteti preciza dupa cat timp va pornii timerul
.NET framework are mai multe timere:
System.Threading.Timer
System.Windows.Forms.Timers
System.Timer.Timer
65
Timer tm = new Timer(new TimerCallback(TimerTick), null, 0, 1000);
//timer-ul porneste imediat si suna la 1 sec
tm.Change(Time.Infinite, 1000);
66
67