Introducere in threaduri WinApi(C++)
Ce sunt threadurile?
Threadurile sunt fire de executie separate care exista in interiorul unei aplicatii.
Firele de executie multiple, permit unei aplicatii sa faca(sau sa dea senzatia ca fac) mai multe lucruri in acelasi
timp.
Acest efect este atins prin permiterea fiecarui fir de executie o anumita perioada de lucru in procesor.
In majoritatea limbajelor de programare moderne(care in general incorporeaza concepte de programare
orientata pe obiect), threadurile sunt portretizate sub forma de obiecte, threadurile respective avand
caracteristici, si metode de utilizare. In contrast, threadurile din WinApi sunt mult mai bazice. In esenta un
thread in WinAPi este doar o simpla functie, ce se executa separat de threadul principal. Acest lucru, pe
langa ca le face un pic mai complicat de folosit le face insa si mai versatile, permitand programatorului sa
creeze pe baza facilitatilor respective, alte reprezentari ale threadurilor.
Cam atat cu introducerea, deci hai sa mergem mai departe cu un pic de cod.
#include <iostream> #include <windows.h> using namespace std; DWORD WINAPI trdFnk(LPVOID lpParam) { for(int i = 0; i< 100;i++) { cout << i << endl; } } int main() { int data = 0; DWORD identificatorTRD = 0; HANDLE trdHdl = CreateThread(NULL, 0, trdFnk, &data, 0, &identificatorTRD); if(trdHdl == NULL) { cout <<"Eroare la creere thread, iesim." << endl; return 0; } WaitForSingleObject(trdHdl, INFINITE); return 0; }Ce face codul asta?
Creaza un thread in care se face o enumerare dupa care iese. Creerea unui tred e formata din 2 pasi.
1. O functie(ce va fii threadul nostru) care are definitia urmatoare: DWORD WINAPI
nume_functie(LPVOID lpParam). Este necesar ca functia noastra thread sa respecte aceasta definitie pentru
a fi recunoscuta de catre al 2-lea pas si anume:
2. Functia CreateThread, aceasta are urmatoarea definitie:
HANDLE WINAPI CreateThread( _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId );
Functia returneaza un obiect de tip HANDLE. Acesta nu este altceva decat un pointer
void(typedef PVOID HANDLE). Un pointer void poate fii folosit pentru a indica catre orice alt tip de
obiect.
Argumente:
-_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes: - LPSECURITY_ATTRIBUTES este
o structura cu 3 membri. Structura este folosita pentru a seta diferite atribute de securitate ale unei
aplicatii sau fir de executie(thread). Structura e definita cam asa:
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle; } SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
-DWORD nLength contine marimea acestei structuri.
-De aici membrul important este LPVOID lpSecurityDescriptor, care este o alta structura nedocumentata.
-BOOL bInheriteHandle seteaza daca alte procese /threaduri create din threadul curent sa ii mosteneasca atributele de securitate.
-_In_ SIZE_T dwStackSize: - stocheaza marimea initiala a stivei firului de executie(marimea in memorie pe care firul de executie o ocupa), daca acest parametru este 0, atunci firul de executie va avea aceeasi marime ca si procesul/firul de executie parinte. Marimea stivei firului de executie se va modifica pe parcursul rularii firului, aceasta marime putand sa scada sau sa creasca.
-_In_ LPTHREAD_START_ROUTINE lpStartAddress - este de fapt numele functiei care reprezinta
firul nostru de executie.
-_In_opt_ LPVOID lpParameter: - este un alt pointer void ce poate indica catre orice tip de obiect, aici puteti sa puneti, fie de unul singur sau daca este necesa trimiterea mai multor variabile, acestea se pot pune
intr-un struct.
-_In_ DWORD dwCreationFlags: - reprezinta unul din flagurile/starile in care threadul este
creeat - 0 inseamna ca threadul ruleaza imediat ce a fost creat. - CREATE_SUSPENDED inseamna ca threadul este creeat intr-o stare suspendata, acesta incepe sa ruleze doar dupa ce functia ResumeThread a fost chemata - STACK_SIZE_PARAM_IS_A_RESERVATION, daca acest parametru este specificat,
atunci dwStackSize va reprezenta marimea initiala a stive, daca nu este specificat va reprezenta marimea
memoriei rezervate pentru firul respectiv de executie. Memoria rezervata este cantitatea de memorie la care
sistemul de operare garanteaza ca firul de executie va avea acces.
-_Out_opt_ LPDWORD lpThreadId: - reprezinta id-ul firului de executie, cu acest id firul de executie
poate fi identificat in interiorul aplicatiei. Acest id este utilizat pentru functii de genul OpenThread, functie
folosita pentru a deschide si accesa obiectul unui thread. Acest id este creat in functia CreateThread si stocat in acest parametru.
Pentru a creea un thread suspendat,atunci cand nu dorim sa porneasca imediat, vom folosi parametrul
CREATE_SUSPENDED Ceva de genul acesta:
#include <iostream> #include <windows.h> using namespace std; DWORD WINAPI trdFnk(LPVOID lpParam) { for(int i = 0; i< 100;i++) { cout << i << endl; } } int main() { int data = 0; DWORD identificatorTRD = 0; HANDLE trdHdl = CreateThread(NULL, 0, trdFnk, &data, CREATE_SUSPENDED, &identificatorTRD); if(trdHdl == NULL) { cout <<"Eroare la creere thread, iesim." << endl; return 0; } ResumeThread(trdHdl); WaitForSingleObject(trdHdl, INFINITE); return 0; }Ok, sa zicem ca trebuie sa suspendam un thread care ruleaza. Pentru asta va trebui sa apelam urmatoarea
functie: DWORD WINAPI SuspendThread( _In_ HANDLE hThread ); - argumentul necesar este
handle-ul threadului. Acesta este obtinut prin chemarea functiei CreateThread despre care am vorbit putin
mai sus. Bine-inteles, dupa ce am suspendat un thread o sa avem nevoie as il si repornim, corect? Pentru
asta folosim functia DWORD WINAPI ResumeThread( _In_ HANDLE hThread ); - la fel ca si
SuspendThread, ResumeThread primeste ca argument handle-ul threadului. Haide sa vedem un mic exemplu:
#include <iostream> #include <windows.h> using namespace std; DWORD WINAPI trdFnk(LPVOID lpParam) { for(int i = 0; i< 100;i++) { cout << i << endl; Sleep(100); // Am adaugat un mic timer ca threadul sa nu termine treaba asa repede :) } } int main() { int data = 0; DWORD identificatorTRD = 0; HANDLE trdHdl = CreateThread(NULL, 0, trdFnk, &data, 0, &identificatorTRD); if(trdHdl == NULL) { cout <<"Eroare la creere thread, iesim." << endl; return 0; } cout << "Suspendam threadul... "; Sleep(5000); // asteptam 5 secunde dupa care suspendam threadul. SuspendThread(trdHdl); cout << "Threadul a fost suspendat, asteptam inca 5 secunde dupa care il repornim."; Sleep(5000); ResumeThread(trdHdl); WaitForSingleObject(trdHdl, INFINITE); return 0; }Atunci cand lucram cu threadurile, se poate intampla, daca codul nu este scris corect, sa avem situatii in care
2 threaduri incearca sa acceseze aceeasi variabila sau aceleasi date. Desigur asta e problema cea mai
comuna, pot aparea multe altele. Acest lucru tine de sincronizarea datelor si threadurilor. Winapi pune la
dispozitie 5 obiecte care ajuta la contrulul si efectuarea sincronizarii. Acestea sunt mutex, event,
semaphore si waitableTimer si critical_section.
Sa le luam pe rand sa vorbim un pic despre ele.
Mutex:
- Mutexi sunt folositi pentru a proteja variabile de la a fi accesate si modificate de catre 2 sau mai multe
threaduri/procese in acelasi timp. Ei fac asta prin permiterea unui proces/thread sa preia proprietatea asupra
acelui mutex. Un alt thread care incearca sa modifice variabila respectiva va trebui sa verifice si sa astepte
pana mutex-ul respectiv nu mai este sub proprietatea altui thread/proces pentru a putea sa efectueze
actiunea dorita. Un mutex se creeaza cu ajutorul functiei CreateMutex care are urmatoarea definitie:
HANDLE WINAPI CreateMutex( _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_ BOOL bInitialOwner,
_In_opt_ LPCTSTR lpName );
Parametrii sunt urmatorii:
- _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes este o structura cu 3 membri.
Structura este folosita pentru a seta diferite atribute de securitate ale unei aplicatii sau fir de
executie(thread) Structura e definita cam asa:
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle; } SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
- DWORD nLength contine marimea acestei structuri.
- De aici membrul important este LPVOID lpSecurityDescriptor, care este o alta structura
nedocumentata.
- BOOL bInheriteHandle seteaza daca alte procese /threaduri create din threadul curent sa ii mosteneasca atributele de securitate.
- _In_ BOOL bInitialOwner va specifica daca threadul/procesul care creeaza acest mutex este si
detinatorul lui initial, in felul acesta "inchizand" mutex-ul, caz in care oricare alt thread ce va dori sa il
foloseasca va trebui sa astepte pana cand threadul/procesul creator il va elibera.
-_In_opt_ LPCTSTR lpName - este numele atribuit mutex-ului. Acest nume va fi folosit cu functia
OpenMutex prin care un thread poate sa preia proprietatea asupra unui thread care nu este detinut de
nimeni. Daca functia este executata cu succes atunci aceasta returneaza un handle catre obiectul dorit.
Daca obiectul exista deja(pe baza numelui furnizat ultimului argument) atunci este returnat un handle catre
acest obiect deja existent. In acest caz se poate apela functia GetLastError care va returna
ERROR_ALREADY_EXISTS. In cazul in care functia esueaza, valoarea returnata va fi NULL.
Mai jos avem un mic exemplu ce foloseste mutex-i pentru a proteja o variabila. In acest exemplu creem
2 threaduri, un mutex si un contor(ctr). Threadurile preiau proprietatea asupra mutex-ului pe rand si
incrementeaza variabila dupa care elibereaza mutex-ul.
#include <iostream> #include <windows.h> using namespace std; int ctr = 0; // counterul ce va fi incrementat pe rand de cele 2 threaduri HANDLE mutex; // mutex-ul care va proteja variabila respectiva. DWORD WINAPI trdLoop(LPVOID lpParam) { DWORD dwWaitResult; // va stoca rezultatul actiuni de asteptare pentru obtinerea proprietatii threadului do { dwWaitResult = WaitForSingleObject(mutex, INFINITE); switch(dwWaitResult) { case WAIT_OBJECT_0: { cout << GetCurrentThreadId() << " " << ctr++ << endl; if(ReleaseMutex(mutex) == 0) { cout << "Eroare la eliberare mutex, eroare: " << GetLastError() << endl; } break; } default: { cout << "Eroare nespecificata in threadul " << GetCurrentThreadId() << " " << GetLastError() << endl; } } //cout << i << endl; Sleep(100); // Dormim un pic pentru a nu printa prea repede datele si pentru a da timp si celuilalt thread sa execute, altfel threadurile vor executa // incrementarea aproape la intamplare, in functie de cum primesc timp de procesare pe procesor }while(ctr < 100); } int main() { DWORD identificatorTRD1 = 0; DWORD identificatorTRD2 = 0; // creem intai mutex-ul deoarece daca creem intai threadurile vor porni direct // si vor incerca sa acceseze un mutex care nu exista mutex = CreateMutex(NULL, FALSE, "grMutex"); if(mutex == NULL) { cout << "Eroare la creere mutex, iesim." << endl; return 0; } HANDLE trdHdl1 = CreateThread(NULL, 0, trdLoop, NULL, 0, &identificatorTRD1); if(trdHdl1 == NULL) { cout <<"Eroare la creere thread 1, iesim." << endl; return 0; } HANDLE trdHdl2 = CreateThread(NULL, 0, trdLoop, NULL, 0, &identificatorTRD2); if(trdHdl2 == NULL) { cout << "Eroare la creere thread 2, iesim." << endl; return 0; } WaitForSingleObject(trdHdl1, INFINITE); // asteptam ca threadul1 sa termine WaitForSingleObject(trdHdl2, INFINITE); // asteptam ca threadul2 sa termine // inchidem handle-urile CloseHandle(trdHdl1); CloseHandle(trdHdl2); CloseHandle(mutex); return 0; }CriticalSection(sectiune critica):
- Este asemanator unui mutex, doar ca o sectiune critica poate fi folosita doar in interiorul unui singur
proces, pe cand un mutex, poate fi folosit si la comunicarea intre procese. Procesul de utilizare a unei
sectiuni critice este urmatorul:
- definirea unei structuri de tipul CRITICA_SECTION, in multe cazuri aceasta strucuta va fi globala, dar nu este obligatoriu este important ca threadurile ce se vor folosi de ea, sa o poata accesa, intr-un fel sau altul.
- initializarea acestei structuri cu ajutorul functiei InitializeCriticalSection
- un tread va incerca sa preia controlul asupra unei sectiuni critice cu ajutorul functiei
EnterCriticalSection, care va astepta in cazul in care sectiunea critica este detinuta de alt proces/thread,
sau TryEnterCriticalSection care va returna imediat indiferent daca va putea sau nu sa preia proprietatea
acelei sectiuni critice
- parasirea sectiuni critice cu functia LeaveCriticalSection, aceasta functie trebuie folosita de fiecare data cand folositi una din functiile de intrare.
Haide sa vedem un exemplu: Folosim un exemplu aproape identic cu cel de la Mutex doar ca in cazul acesta folosim o sectiune critica pentru a proteja variabila ctr.
#include <iostream> #include <windows.h> using namespace std; int ctr = 0; // counterul ce va fi incrementat pe rand de cele 2 threaduri CRITICAL_SECTION crit_sect; // sectiunea critica ce va progeja variabila ctr; /** DWORD WINAPI trdFnk(LPVOID lpParam) { for(int i = 0; i< 100;i++) { cout << i << endl; Sleep(100); // Am adaugat un mic timer ca threadul sa nu termine treaba asa repede :) } cout << "Exiting thread\n"; ExitThread((DWORD) 0); } */ DWORD WINAPI trdLoop(LPVOID lpParam) { DWORD dwWaitResult; // va stoca rezultatul actiuni de asteptare pentru obtinerea proprietatii threadului do { EnterCriticalSection(&crit_sect); // intram in sectiunea critica { cout << GetCurrentThreadId() << " " << ctr++ << endl; // efectuam operatia pe care o dorim } LeaveCriticalSection(&crit_sect); // iesim din sectiunea critical //cout << i << endl; Sleep(10); // dam timp si celuilalt thread sa preia controlul aspura sectini critice si sa faca incrementarea // in caz contrar doar primul thread va face incrementarile. }while(ctr < 100); } int main() { DWORD identificatorTRD1 = 0; DWORD identificatorTRD2 = 0; // creem intai seciunea critica deoarece daca creem intai threadurile vor porni direct // si vor incerca sa acceseze o sectiune critiune critica inexistenta InitializeCriticalSection(&crit_sect); // initializam sectiunea critica HANDLE trdHdl1 = CreateThread(NULL, 0, trdLoop, NULL, 0, &identificatorTRD1); if(trdHdl1 == NULL) { cout <<"Eroare la creere thread 1, iesim." << endl; return 0; } HANDLE trdHdl2 = CreateThread(NULL, 0, trdLoop, NULL, 0, &identificatorTRD2); if(trdHdl2 == NULL) { cout << "Eroare la creere thread 2, iesim." << endl; return 0; } WaitForSingleObject(trdHdl1, INFINITE); // asteptam ca threadul1 sa termine WaitForSingleObject(trdHdl2, INFINITE); // asteptam ca threadul2 sa termine // inchidem handle-urile CloseHandle(trdHdl1); CloseHandle(trdHdl2); return 0; }Semaphores:
- Semafoarele sunt obiecte folosite pentru a limita numarul de threaduri care pot avea acces la o resursa
in acelasi timp Sa zicem ca avem o baza de date si vrem sa limitam numarul de conexiuni la acea baza de
date. In cazul acesta vom folosi un semaphore Creerea unui semaphore se face cu ajutorul urmatoarei functii:
HANDLE WINAPI CreateSemaphore( _In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
_In_ LONG lInitialCount,
_In_ LONG lMaximumCount,
_In_opt_ LPCTSTR lpName );
Daca functia este executata cu succes atunci aceasta returneaza un handle catre obiectul dorit. Daca
obiectul exista deja(pe baza numelui furnizat ultimului argument) atunci este returnat un handle catre acest
obiect deja existent. In acest caz se poate apela functia GetLastError care va returna
ERROR_ALREADY_EXISTS. In cazul in care functia esueaza, valoarea returnata va fi NULL.
Parametrii asteptati de aceasta functie sunt urmatorii:
- _In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes la fel ca si in celelalte cazuri de mai sus acest parametru spune ce permisiuni are acest semaphore
- _In_ LONG lInitialCount - numarul initial de threaduri/procese care pot sa acceseze
aceasta resursa. Atunci cand un thread/proces preia proprietatea asupra acestui semaphore aceasta valoare
este decrementata, iar cand threadul respectiv elibereaza semaphore-ul, cu ajutorul functiei
ReleaseSemaphore, aceasta valoare este incrementata. Valoarea aceasta nu poate depasii valoarea
lMaximumCount
- _In_ LONG lMaximumCount - numarul maxim de threaduri/procese care pot accesa aceasta resursa - _In_opt_ LPCTSTR lpName - numele semaphore-ului. Mai jos aveti un exemplu in care mai multe
threaduri incrementeaza o variabila, totusi semaforul nu permite decat la 4 threaduri sa faca incrementarea in
acelasi timp.
In acest exemplu ne folosim de sectiunea critica pentru a ne asigura ca nu sunt 2 threaduri care incearca sa scrie aceeasi variabila in acelasi timp.
#include <iostream> #include <windows.h> using namespace std; int ctr = 0; // counterul ce va fi incrementat pe rand de cele 2 threaduri CRITICAL_SECTION crit_sect; // Sectiune critica ce va proteja variabila ctr de la a fi scrisa de mai multe threaduri odata. HANDLE semafor; // ce se va asigura ca doar 4 threaduri lucreaza in acelasi timp int MAX_SEM = 4; // numarul maxim de threaduri ce pot accesa variabila int MAX_THREADS = 10; // numarul maxim de threaduri DWORD WINAPI trdLoop(LPVOID lpParam) { DWORD dwWaitResult; // va stoca rezultatul actiuni de asteptare pentru obtinerea proprietatii threadului for(int i = 0; i < MAX_THREADS; i++) { dwWaitResult = WaitForSingleObject(semafor, INFINITE); // asteptam ca semaforul sa semnalizeze ca avem permisiunea sa accesam zona respectiva switch(dwWaitResult) { case WAIT_OBJECT_0: { EnterCriticalSection(&crit_sect); // intram in sectiunea critica { cout << GetCurrentThreadId() << " " << ctr++ << endl; // efectuam operatia pe care o dorim } LeaveCriticalSection(&crit_sect); // iesim din sectiunea critical break; } default: { cout << "Caz necunoscut, eroare: " << GetLastError() << endl; } } ReleaseSemaphore(semafor, 1, NULL); //cout << i << endl; Sleep(10); // dam timp si celuilalt thread sa preia controlul aspura sectini critice si sa faca incrementarea // in caz contrar doar primul thread va face incrementarile. } } int main() { DWORD identificatoriThreaduri[MAX_THREADS]; semafor = CreateSemaphore(NULL, MAX_SEM, MAX_SEM, NULL); HANDLE thrds[MAX_THREADS]; if(semafor == NULL) { cout << "Eroare la creere semafor, iesim." << endl; return 0; } // creem intai mutex-ul deoarece daca creem intai threadurile vor porni direct // si vor incerca sa acceseze un mutex care nu exista InitializeCriticalSection(&crit_sect); for(int i = 0; i < MAX_THREADS;i++) // creem cele 10 threaduri { thrds[i] = CreateThread(NULL, 0, trdLoop, NULL, 0, &identificatoriThreaduri[i]); if(thrds[i] == NULL) { cout << "Eroare la creere thread numarul " << i << " iesim. " << endl; return 0; } } WaitForMultipleObjects(MAX_THREADS, thrds, TRUE, INFINITE); // asteptam ca threadurile sa termine. for(int i = 0; i < MAX_THREADS;i++) { CloseHandle(thrds[i]); } return 0; }Waitable time
-Este un obiect folosit in mare parte atunci cand avem nevoie sa verificam sau sa facem ceva la un
anumit interval de timp Sa zicem ca o data la 5 secunde trebuie sa verificam ceva si sa facem ceva dupa,
in cazul acesta s-ar putea folosi un waitable timer.
El este creat cu functia urmatoare:
HANDLE WINAPI CreateWaitableTimer( _In_opt_ LPSECURITY_ATTRIBUTES lpTimerAttributes,
_In_ BOOL bManualReset,
_In_opt_ LPCTSTR lpTimerName );
Daca functia este executata cu succes atunci aceasta returneaza un handle catre obiectul dorit. Daca
obiectul exista deja(pe baza numelui furnizat ultimului argument) atunci este returnat un handle catre acest
obiect deja existent. In acest caz se poate apela functia GetLastError care va returna
ERROR_ALREADY_EXISTS. In cazul in care functia esueaza, valoarea returnata va fi NULL.
Parametri:
- _In_opt_ LPSECURITY_ATTRIBUTES lpTimerAttributes descriptorul de securitate comun celor 5 obiecte folosite in sincronizare
- _In_ BOOL bManualReset specifica daca este un timer ce trebui resetat manual sau se va reseta
singur. Daca valoarea este adevarata atunci timerul va trebui resetat de catre programator dupa efectuarea
unui WaitOnSingleObject/WaitOnMultipleObjects. Daca acest parametru este false atunci timer-ul se va
reseta automat atunci cand are loc cu succes o asteptare pentru una din functiile de asteptare.
- _In_opt_ LPCTSTR lpTimerName numele timer-ului, acesta poate fi NULL Trebuie avut grija cu
aceste obiecte insa, in conditiile in care timer-ul este verificat de prea multe ori, poatet folosi o parte mare din
timpul procesorului, acest lucru ne mai permitand celorlalte threaduri sa execute operatiile lor. Din aceasta
cauza este de preferat, acolo unde se poate sa se foloseasca evenimente, despre care vom discuta un pic
mai jos.
Mai jos aveti un exemplu de folosire al waitable timers:
#include <iostream> #include <windows.h> using namespace std; int ctr = 0; // counterul ce va fi incrementat pe rand de cele 2 threaduri HANDLE timer; // ce se va asigura ca doar 4 threaduri lucreaza in acelasi timp DWORD WINAPI trdLoop(LPVOID lpParam) { DWORD dwWaitResult; // va stoca rezultatul actiuni de asteptare pentru obtinerea proprietatii threadului LARGE_INTEGER timpAsteptare; timpAsteptare.QuadPart = -1000000LL; // 100 milisecunde (1/100) secunde in pasi de 100 nanosecunde do { dwWaitResult = WaitForSingleObject(timer, INFINITE); if(dwWaitResult != WAIT_OBJECT_0) { cout << "Eroare la asteptare: " << GetLastError() << endl; } cout << "Thread " << GetCurrentThreadId() << " " << ctr++ << endl; if(!SetWaitableTimer(timer, &timpAsteptare, 0, NULL, NULL, 0)) { cout << "Eroare la setare timer, iesim. " << endl; return 1; } }while (ctr < 10); } int main() { timer = CreateWaitableTimer(NULL, TRUE, NULL);// creem timer cu resetare manuala LARGE_INTEGER timpAsteptare; HANDLE thrd; DWORD identificatorThread; timpAsteptare.QuadPart = -1000000LL; if(timer == NULL) { cout << "Eroare la creere waitable timer, iesim. " << endl; return 0; } if(!SetWaitableTimer(timer, &timpAsteptare, 0, NULL, NULL, 0)) // il setam la 100 milisecunde { cout << "Eroare la setare timer, iesim. " << endl; return 0; } thrd = CreateThread(NULL, 0, trdLoop, NULL, 0, &identificatorThread); // creem si rula, thread if(thrd == NULL) { cout << "Eroare la creere thread, iesim. " << endl; return 0; } WaitForSingleObject(thrd, INFINITE); //asteptam ca threadul sa termine CloseHandle(thrd); // inchidem threadul CloseHandle(timer); // inchidem timerul return 0; }Evenimente
-Comparativ cu celelalte obiecte de sincronizare nu sunt folosite pentru a proteja variabile comune sau
pentru a face o actiune la un anumit interval de timp. Evenimentele sunt folosite pentru a semnaliza intre
threaduri intalnirea anumitor conditii, incheierea unei anumite actiuni, smd. Procesul de utilizare al
evenimentelor este urmatorul:
- Creere eveniment cu anumite stari(manual reset/auto reset, stare initiala semnalizata sau nesemnalizata
- Setarea evenimentului(in general de catre cel care l-a si creat) - In alt threadu/proces sau in acelasi
thread asteptam ca threadul sa devina semnalizat cu functia WaitForSingleObject, sau daca avem de
asteptat dupa mai multe evenimente cu WaitForMultipleObjects
- Daca evenimentul este creeat cu manual reset in resetam cu ajutorul functiei SetEvent, sau daca
dorim sa il resetam inainte ca acesta sa fie prelucrat cu functia WaitForSingleObject /
WaitForMultipleObjects folosim functia ResetEvent.
Creerea unui event se face cu ajutorul urmatoarei functii:
HANDLE WINAPI CreateEvent( _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName );
Parametri:
- _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes - descriptorul a tributelor de securitate - _In_ BOOL bManualReset specifica daca evenimentul se reseteaza manual(cu functiile specificate
mai sus) sau se reseteaza singur la starea de nesemnalizare
- _In_ BOOL bInitialState specifica daca evenimentul este creeat in stare semnalizata sau
nesemnalizata. Un eveniment, sau orice alt obiect cu care am lucrat pana acum, care este in stare semnalizata
va fi prins de functiile de genul WaitForSingle/MultipleObject. Atunci cand un astfel de obiect este
nesemnalizat atunci functiile de asteptare ruleaza pana cand obiectul este semnalizat
- _In_opt_ LPCTSTR lpName numele evenimentului. In acest caz este de preferat sa numiti
evenimentele, pentru a fi mai usor de utilizat.
Daca functia este executata cu succes atunci aceasta returneaza un handle catre obiectul dorit.
Daca obiectul exista deja(pe baza numelui furnizat ultimului argument) atunci este returnat un handle catre
acest obiect deja existent. In acest caz se poate apela functia GetLastError care va returna
ERROR_ALREADY_EXISTS. In cazul in care functia esueaza, valoarea returnata va fi NULL.
Mai jos avem un mic exemplu. Codul creeaza 2 threaduri, primul thread ruleaza incrementand
variabila ctr, al 2-lea thread asteapta in acest timp ca primul thread sa termine. Cand threadul termina
semnalizeaza acest lucru cu functia SetEvent. Dupa asta al 2-lea thread face si el incrementarile pe care le
are de facut.
#include <iostream> #include <windows.h> using namespace std; int ctr = 0; // counterul ce va fi incrementat pe rand de cele 2 threaduri HANDLE event; // evenimentul nostru DWORD WINAPI trdLoop1(LPVOID lpParam) // primul thread { for(int i = 0; i < 10; i++) { cout << "Thread " << GetCurrentThreadId() << " " << ctr++ << endl; Sleep(100); } if(!SetEvent(event)) // Setam evenimentul ca semnalizat, in acest caz al 2-lea thread primeste confirmarea ca am terminat si poate incepe sa ruleze { cout << "Eroare la setare eveniment, iesim." << endl; return 0; } } DWORD WINAPI trdLoop2(LPVOID lpParam) // al 2-lea thread { DWORD dwWaitResult = WaitForSingleObject(event, INFINITE); if(dwWaitResult == WAIT_OBJECT_0) { for(int i = 0; i < 10; i++) { cout << "Thread " << GetCurrentThreadId() << " " << ctr++ << endl; Sleep(100); } } } int main() { DWORD identificatorTrd1; DWORD identificatorTrd2; event = CreateEvent(NULL, TRUE, FALSE, "TRD1Finish"); // creem evenimentul si ne asiguram ca e corect creat if(event == NULL) { cout << "Eroare la creere eveniment, iesim. " << endl; return 0; } HANDLE trd1; HANDLE trd2; trd1 = CreateThread(NULL, 0, trdLoop1, NULL, 0, &identificatorTrd1); // creem thread 1 if(trd1 == NULL) { cout << "Eroare la creere thread1, iesim. " << endl; return 0; } trd2 = CreateThread(NULL, 0, trdLoop2, NULL, 0, &identificatorTrd2); // creem thread 2 if(trd2 == NULL) { cout << "Eroare la creere thread2, iesim. " << endl; return 0; } WaitForSingleObject(trd1, INFINITE); WaitForSingleObject(trd2, INFINITE); CloseHandle(trd1); CloseHandle(trd2); CloseHandle(event); return 0; }
Pana acum am vorbit despre creerea, resumarea si inchiderea unui thread, si despre elementele de
sincronizare a datelor si threadurilor. Totusi in toate cele 3 cazuri am o functie pe care nu am explicat-o.
Aceasta este WaitForSingleObject(trdHdl, INFINITE); De fapt functia aceasta ce face? Asteapta dupa un semnal de la unul din posibilele handle-uri pe care le accepta, si anume : thread, mutex, event, semaphore si sectiune critica.
Pentru threade-uri semnalul asteptat este de incheiere al executiei
Pentru mutex asteapta ca mutex-ul sa semnalizeze ca nu mai este "inchis"/"detinut" de catre un alt proces/thread
Pentru semaphores asteapta pana cand exista un loc liber in lista cu obiecte ce detin proprietatea asupra semaforului.
Pentru sectiuni critice actioneaza la fel ca si in cazul mutexilor
Pentru evenimente asteapta ca avenimentul respectiv sa devina semnalizat.
Functia este cam asa:
DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);
- _In_ HANDLE hHandle este dupa cum am spus mai sus, handle-ul threadului pentru care dorim sa
asteptam sa termine.
- _In_ DWORD dwMilliseconds este perioada de timp pe care programul sa o astepte. Acest al 2-lea argument poate primi 3 valori:
- valoarea zero caz in care pur si simplu verifica daca threadul a semnalizat ca si-a incheiat procesul, dupa care functia iese
- o valoare nonzero pozitiva, in milisecunde dupa cum numele sugereaza
- valoarea INFINITE caz in care functia va astepta pana cand threadul va semnaliza iesirea(dupa cum am facut noi acum)
Functia asta poate returna 4 valori:
- WAIT_ABANDONED este returnat cand in thread termina executia inainte sa elibereze un mutex pe care il detine
- WAIT_OBJECT_0 este returnat atunci cand threadul asteptat si-a semnalizat incheierea executiei
- WAIT_TIMEOUT este returnat atunci cand parametrului dwMilliseconds ii este furnizata o valoare diferita de 0 si de INFINITE, iar aceasta perioada trece
- WAIT_FAILED indica faptul ca a aparut o eroare in asteptarea threadului respectiv. Pentru a vedea ce eroare am primit se poate apela functia GetLastError. Atentie, aceasta functie trebuie apelata imediat
dupa executia functiei aceasta deoarece orice alta functie din WinApi ce poate intalni o eroare va inlocui eroarea dorita de noi.
Exista o functie sora a acesteia, care asteapta un anumit set de threaduri sa termine executia
Functia arata cam asa:
DWORD WINAPI WaitForMultipleObjects(
_In_ DWORD nCount,
_In_ const HANDLE *lpHandles,
_In_ BOOL bWaitAll,
_In_ DWORD dwMilliseconds
);
- _In_ DWORD nCount reprezinta numarul de handle-uri pentru care functia trebuie sa astepte. Acest trebuie sa aiba valoarea mai mare decat 0, si in mod normal ar trebui sa fie egal cu numarul de elemente din lpHandles
- _In_ const HANDLE *lpHandles este un pointer catre o matrice ce contine handle-urile catre threadurile noastre. Aveti grija, in aceasta matrice nu trebuie sa existe handle-uri care sa indice catre acelasi
thread. De asemenea aveti grija sa nu inchideti threadurile cu CloseHandle inainte ca acestea sa termine,
deoarece functia nu va mai functiona corect. In acest caz este vorba de "comportament nedefinit".
- _In_ BOOL bWaitAll specifica daca functia sa astepte dupa toate threadurile sau doar dupa 1 din ele. Daca parametrul este TRUE atunci functia va astepta dupa absolut toate threadurile din matrice. Daca parametrul este FALSE atunci functia va incheia executa in momentul in care primul thread semnalizeaza ca
si-a incheiat executia.
- _In_ DWORD dwMilliseconds reprezinta perioada pe care functia o va astepta, la fel ca si la functia WaitForSingleObject si aceasta va accepta aceleasi valori:
- valoarea zero caz in care pur si simplu verifica daca threadul a semnalizat ca si-a incheiat procesul, dupa care functia iese
- o valoare nonzero pozitiva, in milisecunde dupa cum numele sugereaza
- valoarea INFINITE caz in care functia va astepta pana cand threadul va semnaliza iesirea(dupa cum am facut noi acum)
Functia aceasta poate returna urmatoarele valori:
- WAIT_OBJECT_0 daca parametrul bWaitAll este TRUE. Asta inseamna ca toate threadurile au semnalizat ca si-au incheiat executia.
- WAIT_OBJECT_0 + (nCount - 1) daca parametrul bWaitAll este false. In cazul asta valoarea returnata va fi suma dintre WAIT_OBJECT_0 si indexul la care se afla threadul ce a semnalizat incheierea
executiei. Evident, pentru a afla care este indexul threadului respectiv in matricea cu threaduri este suficient sa scadem din numarul returnat valoarea WAIT_OBJEC_0
- WAIT_ABANDONED_0 daca bWaitAll este TRUE indica faptul ca cel putin unul din handle-uri
este un mutex abandonat(vezi aceeasi valoare de return la WaitForSingleObject) si ca celelalte threaduri au
semnalizat incheierea executiei
- WAIT_ABANDONED_0 daca bWaitAll este FALSE indica indexul unui mutex care a fost abandonat(procesul/threadul care l-a creeat si-a terminat executia sau a fost inchis) dar care totusi este in stare semnalizata. In acest caz, proprietatea asupra threadului va fi trecuta catre threadul/procesul care a
executat functia de asteptare iar mutex-ul va fi trecut in stare nesemnalizata.
- WAIT_TIMEOUT este returnat atunci cand perioada stabilita prin parametrul dwMillisecond este un numar finit iar conditia pusa de parametrul bWaitAll nu este satisfacuta(pentru bWaitAll adevarat, nu toate obiectele asteptate au fost semnalizate, daca bWaitAll este fals, nici
unul din obiectele asteptate nu a semnalizat.
- WAIT_FAILED indica faptul ca a existat o problema in executarea functiei. In acest caz se poate obtine textul erorii cu ajutorul functiei GetLastError.
Cam aici se incheie primul tutorial privind threadurile din WinApi in C++. Vor mai urma cateva tutoriale cu notiuni pe aceasta tema in care vom discuta si alte subiecte
Ca si un fel de tema de acasa incercati sa faceti si voi cate o mini aplicatie in care sa folositi cele invatate in acest tutorial.
Ca si cateva idei, puteti sa faceti un joculet in care sa folositi un waitable timer pentru a permite utilizatorului doar un anumit timp pentru efectuarea unei actiuni,
sau ati putea sa scriei un programel care pur si simplu copiaza un fisier dintr-o parte in alta(intr-un alt thread) iar la final threadul ce face copierea sa semnalizeze incheierea printr-un event.
Sper ca v-a placut tutorialul si ca va v-a ajuta. Daca aveti intrebari nu eziatati sa le puneti.
No comments:
Post a Comment