API-ul Win32 include o familie de functii numite
::InterlockedIncrement,
::InterlockedDecrement,
::InterlockExchange,
::InterlockCompareExchange
::InterlockExchangeAdd
pe care le putem folosi pentru a opera asupra in siguranta asupra unor valori pe 32 de biti fara sa folosim in mod explicit obiecte de sincronizare. De exemplu, daca nVar este un UINT, DWORD sau alte tipuri de date pe 32 de biti, putem sa o incrementam cu declaratia
::InterlockedIncrement (& nVar);
si sistemul se va asigura ca nici un alt acces la nVar folosind functii Interlocked nu se va suprapune cu acesta. nVar va trebui sa fie aliniata pe o limita de 32 de biti, deoarece altfel functiile Interlocked s-ar putea sa dea gres pe sistemele Windows NT cu mai multe procesoare. De asemenea, ::InterlockCompareExchange si ::InterlockExchangeAdd sunt suportate doar in Windows NT 4.0 sau mai recent si in Windows 98.
O sectiune critica este o mica portiune de cod care cere acces exclusiv la o resursa comuna inainte de executia codului. Aceasta este modalitatea de a manipula "atomic" o resursa. Prin atomic intelegem faptul ca codul stie ca nici un alt fir de executie va accesa resursa. Bineinteles, sistemul poate porni alt fir de executie si poate planifica altele. Totusi, el nu va planifica fire care doresc sa acceseze resursa pana cand firul nostru nu paraseste sectiunea critica.
Care sunt punctele de reper in folosirea sectiunilor critice ? Atunci cand avem o resursa care este accesata de multiple fire de executie, trebuie sa cream o structura CRITICAL_SECTION.
Daca avem resurse care sunt intotdeauna folosite impreuna, putem crea o singura structura CRITICAL_SECTION care sa le pazeasca pe toate.
Daca avem resurse multiple care nu sunt utilizate tot timpul impreuna, trebuie sa cream o structura CRITICAL_SECTION pentru fiecare resursa.
In cazul in care avem o sectiune critica, trebuie sa apelam EnterCriticalSection careia ii transmitem adresa structurii CRITICAL_SECTION care identifica resursa. Structura CRITICAL_SECTION identifica ce resursa vrea firul sa acceseze iar EnterCriticalSection verifica daca acea resursa este disponibila.
Daca EnterCriticalSection vede ca nici un alt fir nu acceseaza resursa, atunci permite accesul firului apelant. Daca functia vede ca exista un fir de executie care acceseaza resursa, firul apelant trebuie sa astepte pana la eliberarea resursei.
Atunci cand un fir executa cod care nu acceseaza resursa, trebuie sa apeleze LeaveCriticalSection. Aceasta este modalitatea prin care firul spune sistemului ca nu mai vrea sa acceseze resursa. Daca uitam sa apelam LeaveCriticalSection, sistemul va crede ca resursa este ocupata si nu va permite accesul altor fire de executie.
Este foarte important sa impachetam codul care vrea sa acceseze resursa in interiorul functiilor EnterCriticalSection si LeaveCriticalSection. Daca uitam sa facem acest lucru chiar si doar intr-un singur loc resursa va fi predispusa la corupere.
Atunci cand nu putem rezolva problema de sincronizare cu ajutorul functiilor de sincronizare, trebuie sa incercam sa folosim sectiunile critice. Avantajul sectiunilor critice este ca sunt usor de folosit si folosesc functiile de sincronizare intern, astfel incat se executa foarte rapid. Dezavantajul major este ca nu le putem folosi pentru sincronizarea firelor in procese diferite.
Daca incercam sa cautam structura CRITICAL_SECTION in documentatia SDK, nu vom gasi nici macar o intrare despre acest subiect. Microsoft a considerat ca nu e nevoie sa intelegem aceasta structura. Pentru noi, aceasta structura este impenetrabila - structura este documentata dar variabilele membru din interiorul ei nu sunt. Bineinteles, de vreme ce aceasta este o structura de date, putem cauta in fisierele de definitii ale Windows-ului si putem vedea variabilele membre. Dar nu trebuie sa scriem cod care se refera direct la aceste variabile.
Pentru a manevra aceasta structura, apelam o functie Windows, transmitandu-i adresa structurii. Functia stie cum sa manipuleze membrii si garanteaza ca starea structurii este intotdeauna consistenta.
In mod normal, structura CRITICAL_SECTION este alocata ca o variabila globala pentru a oferi tuturor firelor din proces o modalitate simpla de a referi structura; prin numele variabilei. Totusi, structura CRITICAL_SECTION poate fi alocata ca o variabila locala sau poate si alocata dinamic in heap . Sunt doar doua cerinte. Prima este ca toate firele care vor sa acceseze resursa trebuie sa stie adresa structurii CRITICAL_SECTION care protejeaza resursa. Putem transmite aceasta adresa firelor prin ce mecanism dorim. A doua cerinta este ca membrii din structura CRITICAL_SECTION trebuie sa fie initializati inainte ca alte fire sa incerce sa acceseze resursa. Structura este initializata in modul urmator :
VOID InitializeCriticalSection (PCRITICAL_SECTION pcs);
Aceasta functie initializeaza membrii structurii CRITICAL_SECTION. Deoarece aceasta functie doar seteaza niste variabile membru, nu poate esua si de aceea valoarea de retur este VOID. Aceasta functie trebuie apelata inainte ca orice fir de executie sa apeleze EnterCriticalSection. Platforma SDK mentioneaza clar ca rezultatele nu sunt definite daca un fir incearca sa intre intr-o structura CRITICAL_SECTION neinitializata.
Atunci cand suntem siguri ca firele de executie nu vor mai incerca sa acceseze resursa comuna, trebuie sa curatam structura CRITICAL_SECTION prin urmatorul apel :
VOID DeleteCriticalSection (PCRITICAL_SECTION pcs);
DeleteCriticalSection examineaza variabilele membru din interiorul structurii. Aceste variabile indica ce fir de executie, daca exista unul, acceseaza in momentul respectiv resursa comuna.
EnterCriticalSection efectueaza urmatoarele teste :
daca nici un fir nu acceseaza resursa, EnterCriticalSection actualizeaza variabilele membru pentru a indica faptul ca firul apelant a primit dreptul de a accesa resursa si apoi returneaza imediat, permitand firului sa isi continue executia.
daca variabilele membru indica faptul ca firul apelant a primit deja dreptul de accesa resursa, EnterCriticalSection actualizeaza variabilele pentru a indica de cate ori a primit accesul la resursa firul apelant si apoi functia returneaza imediat, permitand firului sa isi continue executia. Aceasta situatie este rar intalnita si apare doar atunci cand firul apeleaza EnterCriticalSection de doua ori la rand fara a apela LeaveCriticalSection.
daca variabilele membru indica faptul ca un fir (altul decat cel apelant) a primit dreptul de a accesa resursa, EnterCriticalSection pune firul apelant in starea de asteptare. Acest lucru este benefic deoarece astfel firul nu mai consuma timp de procesor. Sistemul retine ce fire de executie doresc sa acceseze resursa, actualizeaza automat variabilele membru ale structurii CRITICAL_SECTION si permite firului sa devina planificabil de indata ce firul care acceseaza resursa apeleaza LeaveCriticalSection.
EnterCriticalSection nu are o structura interna prea complexa; ea efectueaza doar cateva teste simple. Ce face functia aceasta atat de valoroasa este ca poate efectua aceste teste atomic. Daca doua fire apeleaza EnterCriticalSection in acelasi timp pe un sistem multiprocesor, functia se comporta corect : un fir primeste permisiunea sa acceseze resursa iar celalalt este plasat in starea de asteptare.
Daca EnterCriticalSection pune un fir in starea de asteptare, firul s-ar putea sa nu mai fie planificat pentru o perioada mai mare de timp. De fapt, intr-o aplicatie scrisa prost, firul ar putea sa nu mai fie planificat nici o data pentru timp de procesor. Daca acest lucru are loc, spunem ca firul este infometat de timp de procesor.
In realitate, firele care asteapta o sectiune critica nu infometeaza niciodata. Apelurile EnterCriticalSection in cele din urma expira, cauzand aparitia unei exceptii. Putem atasa 717i84h un debugger aplicatiei noastre pentru a determina ce nu a functionat corect. Durata de timp care trebuie sa expire este data de valoarea continuta in registri la adresa :
HKEY_LOCAL_MACHINESystemCurrentControlSetControlSession Manager
Putem folosi urmatoarea functie in locul functiei EnterCriticalSection :
BOOL TryEnterCriticalSection (PCRITICAL_SECTION pcs);
Aceasta functie nu permite firului apelant sa intre in starea de asteptare. In schimb, valoarea returnata ne spune daca firul apelant a fost in stare sa primeasca acces la resursa. Daca TryEnterCriticalSection vede ca resursa este accesata de un alt fir, ea returneaza FALSE. In toate celelalte cazuri ea returneaza TRUE.
Cu aceasta functie, un fir poate verifica rapid daca poate accesa o resursa comuna si in caz contrar continua sa faca altceva. Daca functia returneaza TRUE, variabilele membru ale structurii CRITICAL_SECTION au fost actualizate pentru a reflecta faptul ca firul acceseaza o resursa. De aceea, fiecare apel al functiei TryEnterCriticalSection care returneaza TRUE trebuie sa fie potrivit cu un apel al functiei LeaveCriticalSection.
Windows 98 nu are o implementare utila pentru functia TryEnterCriticalSection. Ea returneaza intotdeauna FALSE.
La sfarsitul codului care acceseaza resursa, trebuie sa apelam aceasta functie :
VOID LeaveCriticalSection (PCRITICAL_SECTION pcs);
LeaveCriticalSection examineaza variabilele membre din interiorul structurii. Functia de decrementeaza cu 1 un contor care indica de cate ori a primit accesul la resursa firul apelant. Daca acest contor este mai mare decat 0, LeaveCriticalSection nu face nimic altceva si returneaza.
Daca contorul devine 0, ea verifica daca mai sunt alte fire in asteptare apeland EnterCriticalSection. Daca macar un fir este in asteptare, actualizeaza variabilele membre si face unul din firele in asteptare planificabil. Daca nici un fir nu este in asteptare, LeaveCriticalSection actualizeaza variabilele membre pentru a indica faptul ca firul nu mai acceseaza resursa.
Ca si functia EnterCriticalSection, functia LeaveCriticalSection efectueaza toate aceste teste si actualizeaza automat. Totusi, LeaveCriticalSection nu plaseaza niciodata un fir in starea de asteptare; ea returneaza imediat.
Atunci cand un fir incearca sa intre intr-o sectiune critica care este detinuta de un alt fir, firul apelant este plasat imediat intr-o stare de asteptare. Acest lucru inseamna ca firul trebuie sa tranziteze din modul utilizator in modul nucleu (aproape 1000 de cicluri de procesor). Aceasta tranzitie este foarte costisitoare. Pe un sistem multiprocesor, firul care este proprietarul curent al resursei se poate executa pe un procesor diferit si poate reda controlul asupra resursei rapid. De fapt, firul care este proprietarul resursei o poate elibera inainte ca celalalt fir sa isi termine tranzitia in modul nucleu. Daca acest lucru are loc, se pierde mult timp de procesor.
Pentru a imbunatati performanta sectiunilor critice, Microsoft a incorporat in ele spinlocks. Astfel, atunci cand apelam EnterCriticalSection, ea intra intr-o bucla folosind un spinlock pentru a incerca sa preia controlul asupra resursei pentru un anumit numar de ori. Doar daca o incercare esueaza firul face tranzitia in modul nucleu pentru a intra in starea de asteptare.
Pentru a utiliza un spinlock cu o sectiune critica, trebuie sa initializam sectiunea critica prin apelul :
BOOL InitializeCriticalSectionAndSpinCount(
PCRITICAL_SECTION pcs,
DWORD dwSpinCount);
La fel ca si functia InitializeCriticalSection, primul parametru al functiei InitializeCriticalSectionAndSpinCount este adresa structurii sectiune critica. Dar in cel de-al doilea parametru, dwSpinCount, transmitem numarul de ori care dorim sa se execute spinlock cat timp incearca sa obtina resursa inainte de a trimite firul in starea de asteptare. Aceasta valoare poate fi orice numar de la 0 la 0x00FFFFFF. Daca apelam aceasta functie cand rulam pe un sistem cu un singur procesor, parametrul dwSpinCount este ignorat si counterul este setat intotdeauna la 0. Acest lucru este bun deoarece setarea unui contor de revenire pe un sistem cu un singur procesor este inutil : firul care este proprietarul resursei nu o poate elibera daca celalalt fir este in bucla.
Putem modifica acest contor folosind urmatoarea functie :
DWORD SetCriticalSectionSpinCount(
PCRITICAL_SECTION pcs,
DWORD dwSpinCount);
Folosirea acestor spinlocks cu sectiuni critice este utila deoarece nu avem nimic de pierdut. Partea cea mai grea este determinarea valorii pentru parametrul dwSpinCount. Pentru cea mai buna performanta, trebuie sa ne jucam cu numerele pana cand suntem satisfacuti de performatele rezultatelor. Ca exemplu, sectiunea critica care pazeste accesul la heap-ul procesului nostru are contorul egal cu 4000.
Probabilitatea ca functia InitializeCriticalSection sa esueze este foarte redusa. Microsoft nu a prevazut acest lucru atunci cand a proiectat-o. Functia poate esua deoarece ea aloca un bloc de memorie pentru ca sistemul sa aiba niste informatie interna pentru depanare. Daca aceasta alocare esueaza, este generata o eroare STATUS_NO_MEMORY.
Putem capta mai usor aceasta problema folosind InitializeCriticalSectionAndSpinCount. Aceasta functie aloca la randul ei memorie dar returneaza FALSE daca nu reuseste.
O alta problema poate sa apara. Intern, sectiunile critice folosesc un obiect nucleu eveniment daca doua sau mai multe fire se lupta pentru sectiunea critica in acelasi timp. Deoarece acest caz este rar intalnit, sistemul nu creeaza obiectul nucleu pana cand nu este necesar prima data. Acest lucru salveaza multe resurse de sistem deoarece majoritatea sectiunilor critice nu au niciodata controverse.
Intr-o situatie cand avem putina memorie, o sectiune critica ar putea avea un conflict si sistemul poate sa nu fie capabil sa creeze obiectul nucleu eveniment cerut. Functia EnterCriticalSection va cauza aparitia exceptiei EXCEPTION_INVALID_HANDLE. Majoritatea programatorilor ignora aceasta eroare posibila si nu au o tratare speciala in codul lor deoarece aceasta eroare este foarte rar intalnita. Totusi, daca dorim sa fim pregatiti pentr asa ceva, avem doua optiuni.
Putem folosi tratarea structurata a erorilor si putem prinde eroarea. Atunci cand apare o eroare, putem fie sa nu accesam resursa protejata de sectiunea critica, fie sa asteptam sa se elibereze memoria si apoi sa apelam EnterCriticalSection din nou.
Cealalta optiune este sa cream sectiunea critica cu InitializeCriticalSectionAndSpinCount si sa ne asiguram ca am setat bitul superior din parametrul dwSpinCount. Atunci cand aceasta functie vede ca acest parametru este setat, creeaza obiectul nucleu eveniment si il asocieaza cu sectiunea critica la momentul initializarii. Daca evenimentul nu poate fi creat, functia returneaza FALSE si putem trata acest lucru mult mai elegant in cod.
Exista mai multe tehnici si informatii folositoare atunci cand lucram cu sectiuni critice.
Prima ar fi folosirea unei singure sectiui critice pentru fiecare resursa
Daca avem cateva structuri de date care nu legatura intre ele, ar trebui sa cream o variabila CRTICAL_SECTION pentru fiecare structura de date. Acest lucru este mai bun decat sa avem o singura structura CRITICAL_SECTION care pazeste accesul la toate resursele.
int g_nNums[100]; // O resursa comuna.
TCHAR g_cChars[100]; // O alta resursa comuna.
CRITICAL_SECTION g_cs; // Pazeste ambele resurse.
DWORD WINAPI ThreadFunc(PVOID pvParam)
LeaveCriticalSection(&g_cs);
return(0);
Acest cod foloseste o singura sectiune critica pentru a proteja tablourile g_nNums si g_cChars atunci cand acestea sunt initializate. Dar aceste tablouri nu au nici o legatura unul cu altul. In timpul executiei buclei, nici un fir nu poate primi acces la nici un tablou. Daca functia ThreadFunc este implementata ca in continuare, cele doua tablouri sunt initializate separat :
DWORD WINAPI ThreadFunc(PVOID pvParam)
Teoretic, dupa ce tabloul g_nNums a fost initializat, un fir diferit care doreste sa acceseze doar tabloul g_nNums si nu la g_cChars poate incepe executia in timp ce ThreadFunc continua sa initializeze tabloul g_cChars. Dar din pacate, acest lucru nu este posibil deoarece o singura sectiune critica protejeaza ambele structuri de date. Pentru a repara acest lucru, putem crea doua sectiuni critice :
int g_nNum[100]; // O resursa comuna
CRITICAL_SECTION g_csNums; // Pazeste g_nNums
TCHAR g_cChars[100]; // O alta resursa comuna.
CRITICAL_SECTION g_csChars; // Pazeste g_cChars
DWORD WINAPI ThreadFunc(PVOID pvParam)
Cu aceasta implementare, un alt fir poate incepe sa foloseasca tabloul g_nNums de indata ce functia ThreadFunc a terminat initializarea lui. Putem de asemenea ca avem un fir care initializeaza tabloul g_nNums si un fir separat initializeaza tabloul g_cChars.
O alta posibilitate este accesarea resurselor multiple simultan.
Uneori avem nevoie sa accesam doua resurse simultan. Daca aceasta a fost o cerinta a functiei ThreadFunc, ea va trebui sa fie implementata ca in modul urmator :
DWORD WINAPI ThreadFunc(PVOID pvParam)
Sa presupunem ca alt fir din proces are nevoie la randul lui sa acceseze tablourile :
DWORD WINAPI OtherThreadFunc(PVOID pvParam)
Tot ce am facut in aceste functii a fost sa schimbam ordinea apelurilor functiilor EnterCriticalSection si LeaveCriticalSection. Dar deoarece aceste doua functii sunt scrise in modul in care sunt scrise, poate aparea un blocaj. Sa presupunem ca functia ThreadFunc isi incepe executia si devine proprietatea sectiunii critice a tabloului g_csChars. Apoi firul care executa OtherThreadFunc primeste timp de procesor si devine proprietar a sectiunii critice a tabloului g_csChars. In acest fel, avem o situatie de blocaj. Atunci cand una din functiile ThreadFunc si OtherThreadFunc incearca sa isi continue executia, nici o functie nu poate deveni proprietara celeilalte sectiuni critice de care are nevoie.
Pentru a rezolva aceasta problema, trebuie sa cerem intotdeauna accesul la resursa in exact aceeasi ordine. Trebuie sa notam ca ordinea nu are importanta atunci cand apelam LeaveCriticalSection deoarece aceasta functie nu are niciodata ca efect intrarea unui fir in starea de asteptare.
Alta sugestie este sa nu tinem sectiunile critice pentru o perioada prea mare de timp.
Atunci cand o sectiune critica este ocupata pentru o perioada mai mare de timp, alte fire s-ar putea sa intre in starea de asteptare, care afecteaza performanta aplicatiei. In continuare avem o tehnica cu care putem sa reducem timpul pierdut in timpul sectiunii critice. Urmatorul cod previne alte fire sa schimbe valoarea din g_s inainte ca mesajul WM_SOMEMSG sa fie trimis ferestrei.
SOMESTRUCT g_s;
CRITICAL_SECTION g_cs;
DWORD WINAPI SomeThread(PVOID pvParam)
Este imposibil sa spunem de cat timp are nevoie functia de fereastra pentru procesarea mesajului WM_SOMEMSG - ar putea fi cateva milisecunde sau cativa ani. In acest timp, nici un alt fir nu poate accesa structura g_s. Este mai bine sa scriem cod ca mai jos :
SOMESTRUCT g_s;
CRITICAL_SECTION g_cs;
DWORD WINAPI SomeThread(PVOID pvParam)
Acest cod salveaza valoarea in sTemp, o variabila temporara. Probabil putem ghici cat timp are nevoie procesorul pentru a executa aceasta linie - doar cateva cicluri de procesor. Imediat dupa ce variabila temporara este salvata, LeaveCriticalSection este apelata deoarece structura globala nu mai are nevoie sa fie protejata. A doua implementare este mult mai buna decat prima deoarece alte fire sunt oprite sa foloseasca structura g_s pentru doar cateva cicluri de procesor in loc de o perioada nedefinita de timp. Bineinteles, aceasta tehnica presupune ca instantaneul structurii este destul de bun pentru ca functia ferestrei sa o citeasca. De asemenea presupune ca functia de fereastra nu are nevoie sa schimbe membrii structurii.
MFC-ul implementeaza sectiunile critice cu ajutorul clasei CCriticalSection.
CCriticalSection::Lock blocheaza o sectiune critica si CCriticalSection::Unlock o deblocheaza. Sa presupunem ca clasa unui document include o data membru de tip lista inlantuita creata din clasa MFC CList si ca doua fire separate folosesc aceasta lista. Una scrie in lista si alta citeste din ea. Pentru a preveni cele doua fire de a accesa lista in acelasi moment, putem sa o protejam cu o sectiune critica. Urmatorul exemplu foloseste un obiect global CCriticalSection pentru a demonstra cele spuse anterior. (S-au folosit obiecte de sincronizare globale in exemple pentru a ne asigura ca obiectele sunt vizibile in mod egal fiecarui fir in proces, dar obiectele de sincronizare nu trebuie sa aiba domeniul global.)
Date globale
CCriticalSection g_cs;
Firul A
g_cs.Lock();
//Scrie in lista inlantuita
g_cs.Unlock();
Firul B
g_cs.Lock();
//Citeste din lista inlantuita
g_cs.Unlock();
Acum este imposibil ca firele A si B sa acceseze lista inlantuita in acelasi timp deoarece lista este "pazita" de aceeasi sectiune critica.
O forma alternativa a functiei CCriticalSection::Lock accepta o valoare de expirare si unele documentatii MFC precizeaza ca daca ii transmitem o valoare, ea va returna daca perioada expira inainte ca sectiunea critica sa devina libera. Documentatia este gresita. Putem specifica un timp de expirare daca vrem, dar Lock nu va returna pana cand nu se va elibera sectiunea critica.
Este evident de ce o lista inlantuita ar trebui protejata de apelurile unor fire concurente , dar cum ramane cu variabilele simple ? De exemplu, sa presupunem ca firul A incrementeaza o variabila cu declaratia :
nVar++;
si firul B face altceva cu acea variabila. Ar trebui ca nVar sa fie protejata cu o sectiune critica ? In general, da. Ceea ce pare o operatie indivizibila intr-un program C poate fi compilata intr-o secventa de cateva instructiuni masina. Si un fir poate primi accesul inaintea altuia intre orice doua secventa masina. Ca o regula, este o idee buna de a proteja orice data de accesul simultan pentru scriere sau pentru accesul simultan pentru scriere si citire. O sectiune critica este instrumentul perfect pentru acest lucru.
Dintre toate obiectele nucleu, evenimentele sunt cele mai primitive. Ele contin un contor de utilizare, o valoare de tip Boolean care indica daca evenimentul este cu resetare manuala sau automata, si o alta valoare de tip Boolean care ne spune ca obiectul este in starea semnalata sau nesemnalata.
Evenimentul devine semnalat atunci cand o operatie s-a terminat. Exista doua tipuri de obiecte eveniment : cu resetare manuala sau cu resetare automata. Atunci cand un eveniment cu resetare manuala este semnalat, toate firele care asteapta obiectul devin planificabile. Atunci cand un eveniment cu resetare automata este semnalat, doar unul din firele care asteapta evenimentul devine planificabil.
Evenimentele reprezinta cea mai folosita metoda atunci cand un fir face o operatie de initializare si apoi semnaleaza unui alt fir sa efectueze restul sarcinii. Evenimentul este initializat ca nesemnalat, iar dupa ce firul isi termina sarcina initiala, seteaza evenimentul la semnalat. In acest moment al executiei, un alt fir, care astepta evenimentul, observa ca evenimentul este semnalat si devine planificabil. Al doilea fir stie ca primul fir si-a efectuat sarcina.
Functia CreateEvent creeaza un obiect nucleu eveniment :
HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL fManualReset,
BOOL fInitialState,
PCTSTR pszName);
Parametrul fManualReset este o valoare de tip Boolean care spune sistemului daca sa creeze un eveniment cu resetare manuala (TRUE)sau un eveniment cu resetare automata (FALSE). Parametrul fInitialState indica daca evenimentul trebuie semnalat (TRUE) sau nesemnalat (FALSE). Dupa ce sistemul creeaza obiectul, CreateEvent returneaza identificatorul obiectului relativ la proces. Firele din alte procese pot avea acces la obiect apeland CreateEvent folosind aceeasi valoare trimisa in parametrul pszName folosind mostenirea, functia DuplicateHandle, apeland OpenEvent sau precizand un nume in parametrul pszName care se potriveste cu numele precizat in apelul CreateEvent :
HANDLE OpenEvent(
DWORD fdwAccess,
BOOL fInherit,
PCTSTR pszName);
Ca intotdeauna, trebuie sa apelam functia CloseHandle atunci cand nu mai avem nevoie de obiectul nucleu.
O data ce un obiect nucleu este creat, putem controla direct starea sa. Atunci cand apelam SetEvent, schimbam starea evenimentului la semnalat :
BOOL SetEvent(HANDLE hEvent);
Atunci cand apelam ResetEvent, setam starea evenimentului la nesemnalat :
BOOL ResetEvent(HANDLE hEvent);
Microsoft a definit un efect secundat pentru asteptarea reusita pentru un obiect cu resetare automata : el este automat resetat la starea nesemnalata atunci cand un fir efectueaza o asteptare reusita a obiectului. De obicei nu este necesar sa apelam ResetEvent pentru un eveniment cu resetare automata deoarece sistemul reseteaza automat evenimentul. Microsoft nu a definit un efect secundar pentru o asteptare reusita pentru evenimentele cu resetare manuala.
Avem in continuare un exemplu pentru sincronizarea firelor de executie folosind obiectele nucleu eveniment :
Cream un identificator global la un eveniment nesemnalat si cu resetare manuala
HANDLE g_hEvent;
int WINAPI WinMain()
DWORD WINAPI WordCount(PVOID pvParam)
DWORD WINAPI SpellCheck (PVOID pvParam)
DWORD WINAPI GrammarCheck (PVOID pvParam)
La pornirea procesului, el creeaza un obiect cu resetare manuala in starea nesemnalata si salveaza identificatorul intr-o variabila globala. Acest lucru face usureaza accesarea aceluiasi obiect eveniment de catre alte fire din acest proces. Cream trei fire de executie. Aceste fire asteapta pana cand continutul unui fisier este citit in memorie, iar apoi fiecare fir acceseaza datele : un fir efectueaza o numarare a cuvintelor, un altul ruleaza verificatorul de sintaxa, iar ultimul lanseaza verificatorul gramatical. Codul pentru aceste fire incepe in mod identic : fiecare fir apeleaza WaitForSingleObject, care suspenda firul pana cand continutul fisierului este citi in memorie de catre firul principal.
De indata ce firul primar are datele, el apeleaza SetEvent, care semnalizeaza evenimentul. In acest moment al executiei, sistemul face toate cele trei fire planificabile - toate primesc timp de procesor si acceseaza blocul de memorie. Trebuie sa remarcam ca toate cele trei fire vor accesa memoria in mod read-only. Acesta este singurul motiv pentru care toate cele trei fire pot rula simultan. De asemenea, trebuie sa observam ca daca sistemul are mai multe procesoare, toate aceste fire se pot executa simultan, executand un volum are de lucru intr-o perioada scurta de timp.
Daca folosim un eveniment cu resetare automata in loc de unul cu resetare manuala, aplicatia se comporta diferit. Sistemul permite doar unui fir secundar sa devina planificabil dupa ce firul principal apeleaza SetEvent. Din nou, nu avem nici garantie care fir va fi facut planificabil de catre sistem. Celelalte doua fire vor ramane in asteptare.
Firul care devine planificabil are acces exclusiv la blocul de memorie. Putem rescrie codul de mai sus astfel incat fiecare fir sa apeleze SetEvent inainte de a returna :
DWORD WINAPI WordCount(PVOID pvParam)
DWORD WINAPI SpellCheck (PVOID pvParam)
DWORD WINAPI GrammarCheck (PVOID pvParam)
Atunci cand un fir isi incheie accesul exclusiv asupra datelor, el apeleaza SetEvent, care permite sistemului sa faca unul din cele doua fire care asteapta planificabil. Din nou, nu stim care din ele va fi facut planificabil, dar acest fir va avea propriul acces exclusiv la blocul de memorie. Atunci cand acest fir isi termina sarcina, apeleaza la randul lui SetEvent, facand ca al treilea fir sa primeasca si el la randul lui acces exclusiv asupra memoriei.
Putem folosi inca o functie pentru evenimente :
BOOL PulseEvent(HANDLE hEvent);
PulseEvent face un eveniment semnalat si apoi imediat nesemnalat. Daca apelam PulseEvent pentru un eveniment cu resetare manuala, toate firele care asteptau pe acel eveniment devin planificabile. Daca apelam PulseEvent pe un eveniment cu resetare automata, doar unul din fire care asteapta devine planificabil. Daca nu exista nici un fir in asteptare, functia nu are nici un efect.
PulseEvent nu este foarte folositoare. Daca o aplicam in practica, nu putem sti care fire, daca exista, vor vedea acest efect si vor deveni planificabile.
Clasa MFC CEvent incapsuleaza obiectele eveniment Win32. Un eveniment este putin mai mult decat un indicator in nucleul sistemului de operare. La orice moment dat, el poate fi in una din urmatoarele doua stari : setat sau nesetat. Un eveniment setat spunem ca este in starea semnalata, iar un eveniment nesetat spunem ca este nesemnalat. CEvent::SetEvent seteaza un eveniment, iar CEvent::ResetEvent il reseteaza. O functie inrudita, CEvent::PulseEvent seteaza un eveniment si il elibereaza intr-o singura operatie.
Evenimentele sunt uneori descrise ca "declansatoare de fire". Un fir de executie apeleaza CEvent::Lock() pentru a se bloca pe un eveniment si a astepta ca acesta sa devina disponibil. Un alt fir seteaza evenimentul si prin urmare elibereaza firul in asteptare. Setarea evenimentului este asemanatoare actionarii unui declansator : deblocheaza firul in asteptare si ii da posibilitatea sa isi rezume executia. Un eveniment poate avea blocate pe el unul sau mai multe fire de executie, iar daca codul nostru este scris corect, toate firele in asteptare vor fi eliberate atunci cand evenimentul devine setat.
Windows suporta doua tipuri diferite de evenimente : evenimente care se reseteaza automat si evenimente care se reseteaza manual. Diferenta dintre ele este destul de simpla, dar implicatiile sunt importante. Un eveniment cu resetare automata este resetat automat in starea nesemnalata atunci cand un fir blocat pe el este eliberat. Un eveniment care se reseteaza manual nu se reseteaza automat, trebuie resetat prin cod. Regulile pentru alegerea unuia dintre aceste doua tipuri de evenimente si pentru folosirea lor dupa ce am facut alegerea sunt urmatoarele :
Daca doar un fir este declansat de eveniment, folosim un eveniment care se autoreseteaza si eliberam firul in asteptare cu SetEvent. Nu este nevoie sa apelam ResetEvent deoarece evenimentul este resetat automat in momentul cand firul este eliberat.
Daca doua sau mau multe fire vor fi declansate de eveniment, folosim un eveniment cu setare manuala si eliberam toate firele in asteptare cu PulseEvent. Din nou, nu este nevoie sa apelam ResetEvent deoarece PulseEvent reseteaza evenimentul dupa eliberarea firelor.
Este vital sa folosim un eveniment cu resetare manuala pentru a declansa fire mai multe fire de executie. De ce? Deoarece un eveniment cu resetare automata va fi resetat in momentul in care unul din fire este eliberat si din aceasta cauza va declansa doar un fir. Este in aceeasi masura important sa folosim PulseEvent pentru a activa declansatorul unui eveniment cu resetare manuala. Daca folosim SetEvent si ReleaseEvent, nu avem nici o garantie ca toate firele in asteptare vor fi eliberate. PulseEvent nu doar seteaza si reseteaza evenimentul, ci de asemenea se asigura ca toate firele care asteapta acel eveniment sunt eliberate inainte de resetarea evenimentului.
Un eveniment este creat prin construirea unui obiect CEvent. CEvent::CEvent accepta patru parametri, toti optionali. Prototipul functiei este :
CEvent (BOOL bInitiallyOwn FALSE,
BOOL bManualReset = FALSE, LPCTSTR lpszName = NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL)
Primul parametru, bInitiallyOwn, precizeaza ca obiectul este initial semnalat (TRUE) sau nesemnalat (FALSE). Optiunea implicita este buna in majoritatea cazurilor. bManualReset precizeaza daca obiectul este un eveniment cu resetare manuala (TRUE) sau automata (FALSE). Cel de-al treilea parametru, lpszName, atribuie un nume obiectului de tip eveniment. Ca si mutexurile, evenimentele pot fi folosite pentru a coordona fire de executie ruland in procese diferite, iar pentru ca un eveniment sa treaca de granitele proceselor, trebuie sa ii atribuim un alt nume. Daca firul care utilizeaza evenimentul apartine aceluiasi proces, lpszName trebuie sa fie NULL. Ultimul parametru, lpsaAttribute, este un pointer catre o structura SECURITY_ATTRIBUTES care descrie atributele de securitate ale obiectului. NULL accepta atributele implicite de securitate, care sunt suficiente pentru majoritatea aplicatiilor.
Cum folosim evenimentele pentru a sincroniza firele? In continuare este un exemplu care implica un fir (firul A) care umple o zona de date tampon cu date si un alt fir (firul B) care face niste operatii cu acele date. Sa presupunem ca firul B trebuie sa astepte un semnal de la firul A care sa spuna ca zona de date tampon este initializata si pregatita. Un eveniment cu resetare automata este instrumentul perfect pentru aceasta operatie :
// Global data
CEvent g_event; // Eveniment cu resetare automata, initial nesemanalat
// Firul A
InitBuffer (& buffer); // Initializam yona de date tampon
g_event.SetEvent (); // Eliberam firul B
// Firul B
g_event.Lock();
// Asteptam semnalul
Firul B apeleaza pentru a se bloca pe obiectul eveniment. Firul A apeleaza SetEvent atunci cand este pregatit sa elibereze firul B.
Unicul parametru transmis functiei Lock specifica cat timp este dispus sa astepte apelantul, in milisecunde. Valoarea implicita este INFINITE, care inseamna sa insemne atat cat este necesar. O valoare diferita de zero inseamna ca functia Lock a returnat deoarece un obiect a devenit semnalat; o inseamna ca perioada de asteptare a expirat sau a intervenit o eroare. MFC-ul nu face nimic deosebit aici. El pur si simplu reconverteste obiectele nucleu de sincronizare a obiectelor si functiile API care opereaza asupra lor intr-o forma mai mult orientata obiect.
Evenimentele care se reseteaza automat sunt eficiente pentru declansarea firelor singulare, dar ce se intampla daca un fir C ruland in paralel cu firul B face ceva total diferit cu data din buffer? Atunci avem nevoie de un eveniment care se reseteaza automat pentru a elibera firele B si C deoarece un eveniment cu resetare automata ar elibera fie pe unul, fie pe altul, dar nu pe amandoua. Codul pentru a declansa doua sau mai multe fire cu ajutorul unui eveniment cu resetare manuala este urmatorul :
// Date globale
CEvent g_event (FALSE, TRUE); //Nesemnalat, cu resetare manuala
// Firul A
InitBuffer (& buffer); // Initializarea bufferului
g_event.PulseEvent (); // Eliberam firele B si C
// Firul B
g_event.Lock (); // Asteptam semnalul
//Firul C
g_event.Lock (); // Asteptam semnalul
Trebuie sa observam ca firul A utilizeaza PulseEvent pentru a actiona declansatorul, conform cu cea de-a doua regula descrisa mai sus.
In concluzie, folosim evenimente care se reseteaza automat si CEvent::SetEvent pentru a elibera firele singulare blocate pe un eveniment si folosim evenimente cu resetare manuala si CEvent::PulseEvent pentru a elibera fire multiple. Daca respectam aceste reguli, atunci evenimentele ne vor servi intr-un mod capabil si de incredere.
Uneori evenimentele nu sunt folosite ca declansatoare, ci ca mecanisme primitive de semnalizare. De exemplu, poate firul B vrea sa stie daca firul A a terminat o operatie, dar nu vrea sa se blocheze daca raspunsul este negativ. Firul B poate verifica starea unui eveniment fara a se bloca trimitand catre ::WaitForSingleObject identificatorul de fir si o valoare de expirare de timp egala cu 0. Identificatorul firului poate fi extras din membrul de date m_hObject al clasei CEvent :
if (::WaitForSingleObject (g_event.m_hObject, 0) == WAIT_OBJECT_0)
else
Atunci cand folosim un eveniment in acest mod trebuie sa fim atenti deoarece daca firul B verifica evenimentul in mod repetat pana cand devine setat, trebuie sa ne asiguram ca evenimentul este cu resetare manuala si nu unul cu resetare automata. In caz contrar, chiar actul verificarii evenimentului il va reseta.
Obiectele nucleu mutex asigura accesul mutual exclusiv asupra al unui fir asupra unei resurse. De fapt, astfel si-a primit mutexul numele. Un mutex contine un contor de utilizare, un ID de fir si un contor de revenire. Mutexurile se comporta identic cu sectiunile critice, dar mutexurile sunt obiecte nucleu, pe cand sectiunile critice sunt obiecte utilizator. Acest lucru inseamna ca mutexurile sunt mai lente decat sectiunile critice. Dar de asemenea inseamna fire din diferite procese pot accesa un singur mutex si ca un fir poate preciza o valoare de timeout cand asteapta o resursa.
ID-ul de fir identifica care fir din sistem este proprietarul mutexului in acel moment, iar contorul de reintoarcere indica numarul de ori de care firul a fost priprietarul mutexului. Mutexurile au mai multe utilizari si sunt printre cele mai folosite obiecte nucleu. In mod tipic, ele sunt folosite pentru a pazi un bloc de memorie care este accesat de mai multe fire de executie. Daca acele fire ar accesa acel bloc de memorie simultan, data din acel bloc ar fi corupta. Mutexurile asigura ca fiecare fir care acceseaza blocul de memorie are acces exclusiv aasupra blocului astfel incat integritatea datelor este mentinuta.
Regulile pentru mutexuri sunt urmatoarele :
daca ID-ul de fir este 0 (un ID invalid de fir), mutexul nu este detinut de nici un fir si este semnalat;
daca ID-ul de fir este diferit de 0, un fir este propritarul mutexului si mutexul este semnalat;
spre deosebire de toate celelalte obiecte nucleu, mutexurile au cod special in sistemul de operare care le permite sa incalce regulile normale.
Pentru a folosi un mutex, un proces trebuie mai intai sa creeze mutexul apeland CreateMutex :
HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL fInitialOwner,
PCTSTR pszName);
Un alt proces poate obtine un identificator relativ la el insuti la un mutex existent apeland OpenMutex :
HANDLE OpenMutex(
DWORD fdwAccess,
BOOL bInheritHandle,
PCTSTR pszName);
Parametrul fInitialOwner controleaza starea initiala a mutexului. Daca trimitem FALSE (in cele mai multe cazuri), atat ID-ul de fir cat si contorul de revenire sunt setate la 0. Acest lucru inseamna ca mutexul nu este in posesia nimanui si de aceea este semnalat.
Daca trimitem TRUE pentru fInitialOwner, ID-ul de fir al obiectului este setat la ID-ul firului si contorul de revenire este setat la 1. Deoarece ID-ul firului este diferit de 0, mutexul este initial nesemnalat.
Un fir poate primi acces la o resursa comuna apeland o functie wait, careia ii transmite identificatorul unui mutex care pazeste resursa. Intern, functia wait verifica ID-ul firului pentru a vedea daca este egal cu 0 (mutexul este semnalat). Daca ID-ul firului este 0, parametrul de ID de fir este setat la ID-ul firului apelant, contorul de revenire este initializat cu 1, iar firul apelant ramane planificabil.
Daca functia wait detecteaza ca ID-ul de fir nu este 0 (mutexul este nesemnalat), firul apelant intra in starea de asteptare. Sistemul tine minte acest lucru si atunci cand ID-ul de fir al mutexului redevine 0, sistemul seteaza ID-ul de fir la ID-ul firului in asteptare, seteaza contorul de revenire la 1 si permite firului in asteptare sa fie planificabil din nou. Ca intotdeauna, aceste verificari si schimbari asupra obiectului nucleu mutex sunt efectuate atomic.
Pentru mutexuri, exista o exceptie speciala de la regulile normale ale unui obiect semnalat sau nesemnalat. Sa presupunem ca un fir incearca sa astepte un obiect mutex nesemnalat. In acest caz, firul este de obicei plasat in starea de asteptare. Totusi, sistemul verifica sa vada daca firul care incearca sa preia controlul mutexului are acelasi ID de fir ca in interiorul obiectului nucleu. Daca cele 2 ID-uri coincid, sistemul permite firului sa ramana planificabil - chiar daca mutexul era nesemnalat. Acest comportament nu il vom intalni la nici un alt tip de obiecte nucleu. De fiecare data cand un fir asteapta cu succes un mutex, contorul de revenire al firului este incrementat. Singura modalitate ca sa avem contorul de revenire mai mare de 1 este ca firul sa astepte acelasi mutex de mai multe ori, profitand de aceasta exceptie de la regula.
O data ce un fir a asteptat cu succes un mutex, firul stie ca are acces exclusiv la resursa protejata. Orice alt fir care incearca sa preia accesul la resursa (asteptand la un mutex oarecare) este plasat in starea de asteptare. Atunci cand firul care este proprietarul in acel moment al resursei nu mai are nevoie de aceasta, trebuie sa elibereze mutexul apeland ReleaseMutex :
BOOL ReleaseMutex(HANDLE hMutex);
Aceasta functie decrementeaza contorul de revenire al obiectului cu 1. Daca firul isi termina cu succes asteptarea unui mutex de mai multe ori, acel fir trebuie sa apeleze ReleaseMutex de acelasi numar de ori inainte ca, contorul de recursie sa devina 0. Atunci cand contorul de recursie devine 0, ID-ul firului este la randul lui setat la 0 si obiectul devine semnalat.
Atunci cand obiectul devine semnalat, sistemul verifica daca orice alt fir de executie asteapta mutexul. Daca da, sistemul alege in mod "corect" unul din firele care asteapta si il lasa sa preia controlul mutexului. Acest lucru inseamna, bineinteles, ca ID-ul firului este setat la ID-ul firului selectat si contorul de recursie este setat la 1. Daca nici un alt fir nu asteapta acel mutex, mutexul sta in starea semnalata astfel incat urmatorul fir care asteapta mutexul il primeste imediat.
Obiectele mutex sunt diferite de celelalte obiecte nucleu deoarece ele au o notiune de "proprietate a firului de executie". Nici unul din celelalte obiecte nucleu pe care le-am discutat nu retine ce fir de executie l-a asteptat cu succes; doar mutexurile retin acest lucru. Acest concept pentru mutexuri reprezinta motivul pentru care mutexurile au o exceptie speciala de la regula care permite unui fir de executie sa preia controlul unui mutex chiar si atunci cand acesta nu este semnalat.
Aceasta exceptie nu se aplica doar unui fir de executie care incearca sa preia controlul unui mutex, ci de asemenea si firelor care incearca sa elibereze un mutex. Atunci cand un fir apeleaza ReleaseMutex, functia verifica sa vada daca ID-ul firului apelant coincide cu ID-ul de fir in obiectul mutex. Daca cele doua ID-uri coincid, contorul de recursie este decrementat ca mai jos. Daca ele nu coincid, ReleaseMutex nu face nimic si returneaza FALSE (indicand esecul) apelantului. Efectuarea unui apel al functiei GetLastError in acest moment va returna ERROR_NOT_OWNER (incercarea de a elibera un mutex care nu este detinut de apelant).
In concluzie, daca un fir care este proprietarul unui mutex isi incheie executia (folosind ExitThread, TerminateThread, ExitProcess sau TerminateProcess) inainte de a elibera mutexul, ce se intampla cu mutexul si cu celelalte fire care asteapta acel mutex ? Raspunsul este ca sistemul considera ca mutexul este abandonat - firul care il controla nu il mai poate elibera deoarece el si-a incetat executia.
Deoarece sistemul pastreaza evidenta tuturor mutexurilor si a obiectelor nucleu, el stie exact cand mutexurile devin abandonate. Intr-un astfel de caz, sistemul reseteaza automat ID-ul de fir al mutexului la 0 si contorul de recursie la 0. Apoi, sistemul verifica sa vada daca exista fire asteapta mutexul. In caz afirmativ, sistemul alege in mod "corect" un fir in asteptare, seteaza ID-ul de fir la ID-ul firului apelant si seteaza contorul de recursie la 1; firul selectat devine planificabil.
Aceeasi situatie aveam si mai devreme cu exceptia faptului ca functia wait nu returneaza valoarea normala WAIT_OBJECT_0 firului. In schimb, ea returneaza valoarea WAIT_ABANDONED. Aceasta valoare speciala de retur (care se aplica doar mutexurilor) indica faptul ca mutexul pe care il astepta firul era in posesia unui alt fir care si-a incetat executia inainte de a termina de folosit resursa comuna. Aceasta situatie este delicata deoarece firul planificat nu are nici o idee despre integritatea resursei comune. In acest caz este de datoria noastra ca dezvoltatori ce trebuie facut.
In viata reala, majoritatea aplicatiilor nu verifica in mod explicit pentru valoarea de retur WAIT_ABANDONED deoarece foarte rar un fir isi incheie brusc executia.
In MFC mutexurile sunt implementate cu ajutorul CMutex. Sa presupunem ca doua aplicatii folosesc un bloc de memorie comuna pentru a interschimba date. In cadrul acelei memorii comune exista o lista inlantuita care trebuie protejata impotriva acceselor unor fire concurente. Sectiunea critica A nu va functiona deoarece nu poate ajunge in afara granitelor procesului, dar un mutex va reusi sa faca acest lucru. Iata ce trebuie sa facem inainte de a scrie sau de a citi in lista inlantuita :
//Date globale
CMutex g_mutex (FALSE, _T ("MyMutex"));
g_mutex.Lock();
Citire sau scriere in lista
g_mutex.Unlock();
Primul parametru transmis catre constructorul CMutex specifica daca mutexul este initial blocat (TRUE) sau deblocat (FALSE). Al doilea parametru desemneaza numele mutexului, care este obligatoriu daca mutexul este folosit pentru a sincroniza fire in doua procese diferite. Noi alegem numele, dar ambele procese trebuie sa precizeze acelasi nume astfel incat cele doua obiecte CMutex sa referentieze acelasi obiect mutex in nucleul Windows. In mod normal, Lock un fir se blocheaza pe un mutex blocat de alt fir si Unlock elibereaza mutexul pentru ca alte fire sa poata sa il blocheze.
Implicit, Lock va astepta pentru o perioada nedefinita de timp pentru ca un mutex sa devina liber. Putem sa construim un mecanism cu sanse mici de esec specificand un timp maxim de asteptare in secunde. In urmatorul exemplu, firul asteapta pana la 1 minut inainte de accesa resursa protejata de mutex.
g_mutex.Lock (60000);
// Citire sau scriere in lista inlantuita
g_mutex.Unlock();
Valoarea returnata de Lock ne spune de ce apelul functiei a returnat. O valoare diferita de zero inseamna ca mutexul s-a eliberat, iar zero indica faptul ca timpul de asteptare a expirat primul. Daca functia Lock returneaza 0, este in mod normal prudent sa nu accesam resursa comuna deoarece ar putea sa conduca la un acces suprapus. Astfel, codul care foloseste facilitatea de timp maxim de asteptare este in mod uzual structurat astfel :
if (g_mutex.Lock (60000))
Exista o deosebire intre mutexuri si sectiuni critice. Daca un fir blocheaza o sectiune critica si se termina fara sa o elibereze, celelalte fire de executie care asteapta ca sectiunea critica sa se elibereze se vor bloca pentru o perioada nedefinita de timp. Totusi, daca un fir de executie care blocheaza un mutex nu reuseste sa il deblocheze inainte de a se termina, sistemul considera ca mutexul este "abandonat" si il elibereaza automat astfel incat celelalte fire in asteptare sa isi poata continua executia.
Semafoarele sunt folosite pentru numararea resurselor. Ele contin un contor de utilizare, asa cum fac toate obiectele nucleu, dar ele contin de asemenea doua valori suplimentare pe 32 de biti; un contor maxim de resurse si contorul de resurse curent. Contorul maxim de resurse identifica numarul maxim de resurse pe care il poate controla semaforul.; contorul curent de resurse indica numarul de resurse care sunt disponibile.
Pentru a intelege mai bine acest mecanism, sa analizam modul in care o aplicatie poate folosi semafoarele. Sa spunem ca dezvoltam un proces server in care am alocat un buffer care poate pastra cererile clientilor. Am codat marimea bufferului astfel incat poate pastra un numar de maxim cinci clienti la un moment dat. Daca un nou client incearca sa contacteze serverul in timp ce cinci cereri sunt nerezolvate, clientul este respins, este generata o eroare care indica faptul ca serverul este ocupat li clientul trebuie sa incerce mai tarziu. Atunci cand procesul server se initializeaza, el creeaza un stoc de fire ( thread pool ) format din cinci fire, fiecare fir fiind pregatit sa proceseze cereri de la clienti individuali pe masura ce apar.
Initial, nici un client nu a facut nici o cerere, astfel incat serverul nu permite nici unui fir sa fie planificabil. Totusi, daca trei clienti fac cereri simultane, trei fire din acest stoc de fire trebuie sa fie planificabile. Putem trata aceasta monitorizare a resurselor si planificarea firelor foarte simplu folosind un semafor : contorul maxim de resurse este setat la 5 deoarece aceasta este marimea bufferului nostru in acest exemplu. Contorul initial de resurse este 0 deoarece nici un client nu a facut nici o cerere. Pe masura ce cererile clientilor sunt acceptate, contorul curent de resurse este incrementat, iar dupa ce cererile sunt satisfacute, contorul este decrementat.
Regulile pentru un semafor sunt urmatoarele :
daca contorul curent de resurse este mai mare de 0, semaforul este semnalat;
daca contorul curent de resurse este 0, semaforul este nesemnalat;
sistemul nu permite niciodata contorului curent de resurse sa fie negativ;
contorul curent de resurse nu poate fi niciodata mai mare decat contorul maxim de resurse.
Atunci cand folosim un semafor, nu trebuie sa confundam contorul de utilizare al obiectului cu contorul curent de resurse.
Aceasta functie creeaza obiectul nucleu semafor :
HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTE psa,
LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pszName);
Un alt proces poate obtine identificatorul relativ la sine al unui semafor existent apeland OpenSemaphore :
HANDLE OpenSemaphore(
DWORD fdwAccess,
BOOL bInheritHandle,
PCTSTR pszName);
Parametrul lMaximumCount precizeaza sistemului numarul maxim de resurse pe care le poate trata aplicatia noastra. Deoarece acesta este o valoare cu semn pe 32 de biti, putem avea 2147483647 resurse. Parametrul lInitialCount precizeaza cate dintre aceste resurse sunt initial disponibile. La initializarea procesului server pe care l-am creat, nu exista cereri sin partea clientilor, deci apelam CreateSemaphore astfel :
HANDLE hsem = CreateSemaphore(NULL, 0, 5, NULL);
Aceasta functie creeaza un semafor cu un contor maxim de resurse egal cu 5, dar initial sunt disponibile 0 resurse. Intamplator, contorul obiectului nucleu este 1 deoarece de abia am creat acest obiect nucleu; nu trebuie sa facem o confuzie intre contoare. Deoarece contorul curent de resurse este initializat cu 0, semaforul este nesemnalat. Orice fir care asteapta acel semafor sunt astfel puse in starea de asteptare.
Un fir primeste acces la resursa apeland o functie wait, careia ii transmite identificatorul semaforului care pazeste resursa. Intern, functia wait verifica contorul curent de resurse al semaforului si daca acesta este mai mare decat 0 (semaforul este semnalat), contorul este decrementat cu 1 si firul apelant ramane planificabil. Cel mai bun la lucru la semafoare este ca ele efectueaza aceasta operatie de test si setare atomic; adica, atunci cand cerem o resursa de la un semafor, sistemul de operare verifica daca resursa este disponibila si decrementeaza contorul de resurse disponibile fara a lasa alt fir sa interfereze cu acest lucru. Doar dupa ce contorul de resurse a fost decrementat, sistemul permite unui alt fir sa ceara acea resursa.
Daca functia wait determina ca, contorul curent de resurse al semaforului este 0 (semaforul este nesemnalat), sistemul pune firul apelant in starea de asteptare. Atunci cand un alt fir incrementeaza contorul curent de resurse al semaforului, sistemul isi aduce aminte de firul din starea de asteptare si ii permite acestuia sa devina planificabil (decrementand corespunzator contorul curent de resurse).
Un fir incrementeaza contorul curent de resurse al unui semafor apeland functia ReleaseSemaphore :
BOOL ReleaseSemaphore(
HANDLE hsem,
LONG lReleaseCount,
PLONG plPreviousCount);
Aceasta functie pur si simplu adauga valoarea din lReleaseCount la contorul curent de resurse al semaforului. In mod normal, transmitem valoarea 1 pentru parametrul lReleaseCount, dar acest lucru nu este absolut necesar. Functia returneaza de asemenea valoarea initiala a contorului curent de resurse in *plPreviosCount. Putine aplicatii tin cont de aceasta valoare, astfel incat putem transmite NULL pentru a o ignora.
Uneori este folositor sa stim ca contorul curent de resurse al unui semafor fara sa modificam contorul, dar nu exista nici o functie care face acest lucru.
MFC-ul reprezinta semafoarele cu instantele clasei CSemaphore. Prototipul constructorului acestei clase este :
CSemaphore( LONG lInitialCount = 1, LONG lMaxCount = 1, LPCTSTR pstrName = NULL, LPSECURITY_ATTRIBUTES lpsaAttributes = NULL );
Declaratia
CSemaphore g_semaphore (3, 3);
construieste un obiect de tip semafor care are contorul de resurse initial egal cu 3 (parametrul 1) si contorul maxim egal cu 3 (parametrul 2). Daca semaforul va fi folosit pentru a sincroniza fire din procese diferite, va trebui sa adaugam un al treilea parametru care atribuie un nume semaforului. Un al patrulea parametru optional pointeaza catre o structura de tip SECURITY_ATTRIBUTES (valoarea implicita este NULL). Fiecare fir care acceseaza o resursa controlata de un semafor poate proceda in modul urmator :
g_semaphore.Lock ();
// Accesarea resursei comune
g_semaphore.Unlock ();
Atat timp cat nu mai mult de trei fire incearca sa acceseze resursa in acelasi timp, Lock nu va suspenda firul. Dar daca semaforul este blocat de trei fire si un al patrulea apeleaza Lock, firul se va bloca pana cand unul din celelalte trei fire apeleaza Unlock. Pentru a limita timpul in care functia Lock asteapta ca valoarea contorului de resurse sa devina pozitiva, putem sa transmitem o valoare maxima de asteptare (in milisecunde, ca intotdeauna) functiei Lock.
CSemaphore::Unlock poate fi folosita pentru a incrementa contorul de resurse cu mai mult de 1 si de asemenea pentru a afla care a fost contorul de resurse inainte de apelul Unlock. De exemplu, sa presupunem ca acelasi fir apeleaza Lock de doua ori la rand pentru a revendica doua resurse protejate de un semafor. In loc sa apelam Unlock de doua ori, firul poate face acest lucru astfel :
LONG lPrevCount;
g_semaphore.Unlock (2, & lPrevCount);
Nu exista functii nici in MFC nici in API care sa returneze contorul de resurse al unui semafor, altele decat CSemaphore::Unlock si echivalentul din API ::ReleaseSemaphore.
O folosire uzuala pentru semafoare este de a permite unui grup de m fire accesul la n resurse, unde m este mai mare decat n. De exemplu, sa presupunem ca lansam 10 fire de lucru si le dam la fiecare sarcina de a aduna date. Ori de cate ori un fir umple un buffer cu date, el transmite datele printr-un socket, elibereaza bufferul si reincepe operatia de strangere de date. Acum sa presupunem ca doar trei socketuri sunt disponibile la un moment dat. Daca noi protejam aceste socketuri cu un semafor al carui contor de resurse este 3 si programam fiecare fir astfel incat sa blocheze semaforul inainte de a pretinde accesul la socket, atunci firele nu vor consuma deloc timp din procesor atat timp cat asteapta ca un socket sa devina liber.
MFC-ul include o pereche de clase numite CSingleLock si CMultiLock care au functiile Lock si Unlock proprii. Putem impacheta o sectiune critica, un mutex, un eveniment sau un semafor intr-un obiect CSingleLock si putem folosi CSingleLock::Lock pentru a aplica un blocaj, ca mai jos :
CCriticalSection g_cs;
CSingleLock lock ( & g cs // Impachetarea intr-un CSingleLock
lock.Lock(); // Blocarea sectiunii critice
Exista vreun avantaj pentru blocarea sectiunii critice in acest mod in locul apelarii directe a functiei Lock a obiectului CCriticalSection ? Uneori, da. Sa consideram ceea ce se intampla daca urmatorul cod arunca o exceptie intre apelurile Lock si Unlock :
g_cs.Lock ();
g_cs.Unlock ();
In cazul in care apare o exceptie, sectiunea critica va ramane blocata definitiv deoarece apelul catre Unlock nu va mai avea loc. Putem face insa in felul urmator :
CSingleLock (&g_cs
lock.Lock ();
lock.Unlock ();
Sectiunea critica nu ramane blocata definitiv. De ce? Deoarece obiectul CSingleLock este creat in stiva, destructorul sau este apelat daca apare o exceptie. Acest destructor apeleaza Unlock asupra obiectului de sincronizare blocat. In alte cuvinte, CSingleLock este un instrument foarte util pentru a ne asigura ca un obiect de sincronizare este deblocat chiar si in cazul aparitiei unor exceptii.
CMultiLock este cu totul diferit. Folosind un CMultiLock, un fir poate bloca pe mai multe obiecte de sincronizare in acelasi timp (pana la 64). In functie de cum apeleaza CMultiLock::Lock, un fir se poate bloca pana cand unul din obiectele de sincronizare devine liber sau pana cand toate devin libere.
CMultiLock::Lock accepta trei parametri, toti optionali. Primul specifica o perioada maxima de asteptare (implicit INFINITE). Al doilea parametru specifica daca firul trebuie sa fie trezit cand unul din obiectele de sincronizare este deblocat (FALSE) sau cand toate devin deblocate (TRUE, implicit). Al treilea este o masca de "trezire" care specifica alte conditii care vor trezi firul, de exemplu mesaje WM_PAINT sau mesaje generate de butoanele mouse-ului. Valoarea implicita pentru acest parametru egala cu 0 previne trezirea firului din alte motive decat din cauza eliberarii unuia sau mai multe obiecte de sincronizare sau a expirarii perioadei de asteptare.
Urmatorul exemplu demonstreaza cum un fir se poate bloca pe doua evenimente si un mutex simultan. Trebuie sa fim constienti de faptul ca evenimentele, mutexurile si semafoarele pot fi impachetate in obiecte CMultiLock, dar sectiunile critice nu pot fi.
CMutex g_mutex ;
CEvent g_event
CSyncObject* g_pObjects [3] = ;
// Blocare pana cand toate cele trei obiecte devin semnalate
CMultiLock multiLock (g_pObjects, 3);
multiLock.Lock ();
multiLock.Lock (INFINITE, FALSE);
Daca un fir este deblocat dupa apelul CMultiLock::Lock pentru a se bloca pana cand doar un obiect de sincronizare devine semnalat, este foarte frecvent cazul in care firul va trebui sa stie ce obiect de sincronizare devine semnalat. Raspunsul poate fi obtinut din valoarea returnata de Lock :
CMutex g_mutex ;
CEvent g_event
CSyncObject* g_pObjects [3] = ;
CMultiLock multiLock (g_pObjects, 3) ;
DWORD dwResult = multiLock.Lock (INFINITE, FALSE);
DWORD nIndex = dwResult - WAIT_OBJECT_0 ;
if ( nIndex == 0)
else
else
if ( nIndex == 2)
Trebuie sa fim constienti ca daca trimitem functiei Lock o valoare de expirare, alta decat INFINITE, trebuie sa comparam valoarea returnata cu WAIT_TIMEOUT inainte de a scadea WAIT_OBJECT_0 in cazul in care Lock a returnat din cauza expirarii perioadei de asteptare. De asemenea, daca Lock returneaza deoarece un mutex abandonat a devenit semnalat, trebuie sa scadem WAIT_ABANDONED_0 din valoarea returnata in loc de WAIT_OBJECT_0.
In continuare vom prezenta un exemplu de o situatie in care CMultiLock poate fi folositoare. Sa presupunem ca trei fire separate, firele A, B, C, lucreaza impreuna pentru a pregati date intr-un buffer. Indata ce datele sunt pregatite, firul D trimite datele prin intermediul unui socket sau le scrie intr-un fisier. Totusi, firul D nu poate fi apelat pana cand firele A, B si C nu si-au terminat operatiile curente. Solutia ? Sa cream obiecte eveniment separate pentru a reprezenta firele A, B si C si sa lasam firul D sa foloseasca un obiect CMultiLock pentru a se bloca pana cand toate cele trei evenimente devin semnalate. Pe masura ce fiecare fir isi termina munca, el seteaza obiectul eveniment corespunzator la o stare semnalata. Firul D se blocheaza din aceasta cauza pana cand ultimul din cele trei semnale de fir este terminat.
Functiile wait permit unui fir sa intre voluntar in starea de asteptare pana cand obiectul nucleu specificat devine semnalat. Pe departe cea mai intalnita functie din aceasta familie este
DWORD WaitForSingleObject (
HANDLE hObject,
DWORD dwMilliseconds);
Atunci cand un fir apeleaza aceasta functie, primul parametru, hObject, identifica un obiect nucleu care suporta sa fie semnalat/nesemnalat. Un obiect mentionat in lista in sectiunea precedenta functioneaza excelent. Al doilea parametru, dwMilliseconds, permite firului sa precizeze cat timp este dispus sa astepte pentru ca obiectul sa devina semnalat.
Urmatoarea functie spune sistemului ca firul apelant vrea sa astepte pana cand procesul identificat de hProcess isi termina executia :
WaitForSingleObject (hProcess, INFINITE);
Al doilea parametru spune sistemului ca firul apelant este dispus sa astepte la infinit pana cand procesul isi termina executia.
De obicei transmitem INFINITE pentru al doilea parametru, dar putem trimite orice valoare (in milisecunde). INFINITE este definit ca 0xFFFFFFFF (sau -1). Bineinteles, poate fi periculos sa trimitem INFINITE. Daca obiectul nu devine niciodata semnalat, atunci firul apelant nu se trezeste - este blocat, din fericire fara a consuma resurse de procesor.
In continuare avem un exemplu pentru functia WaitForSingleObject cu al doilea parametru diferit de INFINITE :
DWORD dw = WaitForSingleObject(hProcess, 5000);
switch (dw)
Codul de mai sus spune sistemului ca firul apelant nu trebuie sa fie planificabil pana cand fie procesul specificat sau au trecut 5000 de milisecunde, care are loc prima. Astfel acest apel returneaza in mai putin de 5000 de milisecunde daca procesul se termina sau returneaza in 5000 de milisecunde daca procesul nu s-a terminat. Trebuie sa retinem ca putem transmitem 0 pentru parametrul dwMilliseconds. In acest caz, WaitForSingleObject returneaza imediat.
Valoarea returnata de aceasta functie indica de ce firul apelant a devenit planificabil din nou. Daca obiectul pe care firul il asteapta devine semnalat, valoarea returnata este WAIT_OBJECT_0; daca perioada de timp expira, valoarea returnata este WAIT_TIMEOUT. Daca ii transmitem un parametru invalid, valoarea returnata este WAIT_FAILED. Pentru mai multe informatii putem apela GetLastError.
Functia de mai jos WaitForMultipleObjects este similara cu WaitForSingleObject in afara cazului in care firul apelant sa verifice daca starea semnalata a mai multor obiecte nucleu simultan :
DWORD WaitForMultipleObjects(
DWORD dwCount,
CONST HANDLE* phObjects,
BOOL fWaitAll,
DWORD dwMilliseconds);
Parametrul dwCount indica numarul de obiecte nucleu pe care dorim sa il verifice functia noastra. Aceasta valoare trebuie sa fie intre 1 si MAXIMUM_WAIT_OBJECT (definita ca 64 in fisierele de definitii ale Windows-ului). Parametrul phObjects este un pointer la un tablou de identificatori de obiecte nucleu.
Putem folosi WaitForMultipleObjects in doua moduri diferite - sa permitem firului sa intre in starea de asteptare pana cand unul din obiectele nucleu precizate devine semnalat, sau sa permitem unui fir sa astepte pana cand toate obiectele nucleu precizate devin semnalate. Parametrul fWaitAll spune functiei in ce mod da functioneze. Daca trimitem valoarea TRUE pentru acest parametru, functia nu va permite firului apelant sa se execute pana cand toate obiectele au devenit semnalate.
Parametrul dwMilliseconds functioneaza la fel ca si pentru WaitForSingleObject. Daca in timpul asteptarii perioada de timp expira, functia returneaza oricum. Din nou, INFINITE este trimis in mod uzual pentru acest parametru, dar trebuie sa scriem codul foarte atenti pentru a evita blocajele.
Valoarea returnata de aceasta functie spune apelantului de ce a fost replanificat. Valorile posibile de retur sunt WAIT_FAILED si WAIT_TIMEOUT, a caror semnificatie este evidenta. Daca trimitem TRUE pentru fWaitAll si toate obiectele devin semnalate, valoarea returnata este WAIT_OBJECT_0. Daca trimitem FALSE pentru fWaitAll, functia returneaza de indata ce oricare dintre obiecte devine semnalat. In acest caz, probabil ca vrem sa stim care obiect a devenit semnalat. Valoare de retur este o valoare intre WAIT_OBJECT_0 si WAIT_OBJECT_0 dwCount 1. In alte cuvinte, daca valoarea nu este WAIT TIMEOUT sau WAIT_FAILE, trebuie sa scadem WAIT_OBJECT_0 din valoarea de retur. Membru rezultat este un index in tabloul de identificatori pe care l-am trimis ca al doilea parametru functiei WaitForMultipleObjects . Indexul ne spune care obiect a devenit semnalat.
HANDLE h[3];
h[0] = hProcess1;
h[1] = hProcess2;
h[2] = hProcess3;
DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000);
switch (dw)
Daca trimitem valoarea FALSE pentru parametrul fWaitAll, WaitForMultipleObjects scaneaza identificatorul de la indexul 0 in sus, iar primul obiect care este semnalat termina asteptarea. Acest lucru poate avea niste ramifictii nedorite. De exemplu, firul nostru ar putea sa astepte trei procese copil sa se termine si trimite trei identificatori acestei functii. Daca procesul de la indexul 0 se termina, functia WaitForMultipleObjects returneaza. Acum firul poate face ce are doreste si apoi revine in bucla, asteptand terminarea unui alt proces. Daca firul trimite aceleasi trei argumente, functia returneaza imediat cu WAIT_OBJECT_0 din nou. Daca nu inlaturam identificatorii de la care am primit deja notificari, codul nu va functiona corect.
Pentru unele obiecte nucleu, un apel reusit al functiei WaitForSingleObject sau WaitForMultipleObjects schimba de fapt starea obiectului. Un apel reusit este unul in care functia vede ca obiectul era semnalat si returneaza o valoare relativa la WAIT_OBJECT_0. Un apel este nereusit daca functia returneaza WAIT_TIMEOUT sau WAIT_FAILED. Obiectele nu isi schimba starea niciodata daca apelul este nereusit.
Atunci cand starea unui obiect este schimbata, numim acesta un efect secundar de asteptare. De exemplu, sa spunem ca un fir asteapta un obiect eveniment autoresetabil. Atunci cand acest obiect devine semnalat, functia detecteaza acest lucru si poate returna WAIT_OBJECT_0 firului apelant. Totusi, chiar inainte ca functia sa returneze, evenimentul este setat la starea nesemnalata - efectul secundar al asteptarii reusite.
Acest efect secundar este aplicat obiectului obiectului eveniment autoresetabil deoarece este una dintre regulile pe care Microsoft le-a definit pentru acest tip de obiect. Alte obiecte au efecte secundare diferite, iar unele obiecte nu aun efecte secundare de loc. Obiectele nucleu proces si fir de execdutie nu au nici un efect secundar - adica asteptarea unui astfel de obiect nu schimba niciodata starea obiectului.
Ceea ce face ca functia WaitForMultipleObjects sa fie atat de utila este ca ea efectueaza toate operatiile sale intern. Atunci cand un fir apeleaza WaitForMultipleObjects, functia poate testa starea semnalata a tuturor obiectelor si poate efectua efectele secundare necesare toate ca o singura operatie.
HANDLE h[2];
h[0] = hAutoResetEvent1; // Initially nonsignaled
h[1] = hAutoResetEvent2; // Initially nonsignaled
WaitForMultipleObjects(2, h, TRUE, INFINITE);
Atunci cand apelam WaitForMultipleObjects, ambele obiecte eveniment sunt nesemnalate; acest lucru forteaza ambele fire sa intre in starea de asteptare. Apoi obiectul hAutoResetEvent1 devine semnalat. Ambele fire observa ca evenimentul a devenit semnalat, dar nici unul dintre ele nu se poate trezi deoarece obiectul hAutoResetEvent2 este in continuare nesemnalat. Deoarece nici unul din fire nu si-a incheiat cu succes asteptarea, nu are loc nici un efect secundar asupra obiectului hAutoResetEvent1.
In continuare, obiectul hAutoResetEvent2 devine semnalat. In acest moment, unul din cele doua fire detecteaza ca ambele obiecte pe care le astepta au devenit semnalate. Asteptarea este reusita, ambele obiecte sunt setate la starea nesemnalata, iar firul devine planificabil. Dar cum ramane cu celalalt fir ? El continua sa astepte pana cand observa ca ambele obiecte eveniment sunt semnalate. Chiar daca la inceput a detectat ca hAutoResetEvent1 era semnalat, acum vede acest obiect ca nesemnalat.
Asa cum am mentionat anterior, este important de retinut ca WaitForMultipleObjects functioneaza in mod atomic. Atunci cand verifica starea obiectelor nucleu, nici un alt fir nu poate schimba starea acestora. Acest lucru previne aparitia blocajelor. Altfel ne putem imagina ce s-ar putea intampla daca un fir vede ca hAutoResetEvent1 este nesemnalat si reseteaza evenimentul la starea nesemnalata si apoi celalalt fir vede ca hAutoResetEvent2 este semnalat si il reseteaza la nesemnalat. Ambele fire ar fi blocate : un fir va astepta un obiect care este detinut ce celalalt fir, si viceversa. WaitForMultipleObjects ne asigura ca acest lucru nu va avea loc.
Daca fire multiple de executie asteapta un singur obiect nucleu, ce fir va hotari sistemul ca trebuie trezit atunci cand obiectul devine semnalat ? Raspunsul Microsoft la aceasta intrebare este : "Algoritmul este corect.". Microsoft nu vrea sa dezvaluie algoritmul intern folosit de sistem. Tot ce trebuie sa stim este ca daca mai multe fire de executie sunt in asteptare, fiecare ar trebui sa aiba posibilitatea de a se trezi de fiecare data cand obiectul devine semnalat.
Acest lucru insemna ca prioritatea firelor nu are nici un efect : firul cu prioritatea cea mai mare nu va primi in mod obligatoriu obiectul. De asemenea inseamna ca firul care a asteptat cel mai mult va primi obiectul. Este posibil ca un fir care are obiectul sa faca o bucla si sa il aiba din nou. Totusi, acest lucru nu ar fi corect fata de celelalte fire, asa ca algoritmul incearca sa previna acest lucru. Dar nu exista garantii.
In realitate, alogoritmul folosit de Microsoft este simpla schema "primul venit, primul iesit". Astfel, firul care a asteptat cel mai mult primeste obiectul. Totusi, pot aparea actiuni in sistem care sa modifice acest comportament, facandu-l mai greu de ghicit. De aceea Microsoft nu explica exact modul de functionare al algoritmului. O astfel de actiune este suspendarea unui fir. Daca firul asteapta un obiect si apoi este suspendat, sistemul uita ca firul asteapta un obiect. Aceasta are loc deoarece nu exista nici un motiv sa planificam un fir care este suspendat. Atunci cand firul isi reia executia mai tarziu, sistemul crede ca firul de abia a inceput sa astepte acel obiect.
Atunci cand depanam un proces, toate firele din proces sunt suspendate atunci cand atingem puncte de oprire. Astfel, depanarea unui proces face algoritmul "primul intrat, primul iesit" foarte imprevizibil deoarece foarte des firele sunt suspendate si apoi isi reiau executia.
|