Stări ale firului. Secțiuni critice Finalizarea sincronizării în sistemul de operare Windows

Acest obiect de sincronizare poate fi utilizat numai local în cadrul procesului care l-a creat. Restul obiectelor pot fi folosite pentru a sincroniza firele diferitelor procese. Numele obiectului „secțiune critică” este asociat cu o selecție abstractă a unei părți a codului (secțiunii) programului care efectuează unele operații, a căror ordine nu poate fi încălcată. Adică, o încercare a două fire diferite de a executa simultan codul acestei secțiuni va avea ca rezultat o eroare.

De exemplu, poate fi convenabil să protejați funcțiile de redactare cu o astfel de secțiune, deoarece accesul simultan al mai multor scriitori ar trebui exclus.

Sunt introduse două operații pentru secțiunea critică:

intra in sectiuneÎn timp ce orice thread se află în secțiunea critică, toate celelalte fire nu vor mai aștepta automat când încearcă să intre în el. Un fir care a intrat deja în această secțiune îl poate intra de mai multe ori fără a aștepta ca acesta să fie eliberat.

părăsiți secțiunea Când un fir iese dintr-o secțiune, contorul numărului de intrări ale acestui fir în secțiune este decrementat, astfel încât secțiunea va fi eliberată pentru alte fire numai dacă firul iese din secțiune de câte ori a intrat în ea. Când o secțiune critică este eliberată, va fi trezit doar un fir, așteptând permisiunea de a intra în această secțiune.

În general, în alte API-uri non-Win32 (cum ar fi OS/2), secțiunea critică nu este tratată ca un obiect de sincronizare, ci ca o bucată de cod de program care poate fi executată de un singur fir de aplicație. Adică, intrarea în secțiunea critică este considerată o oprire temporară a mecanismului de comutare a firelor până la ieșirea din această secțiune. API-ul Win32 tratează secțiunile critice ca obiecte, ceea ce duce la o oarecare confuzie -- sunt foarte apropiate în proprietățile lor de obiecte exclusive fără nume ( mutex, vezi mai jos).

Atunci când utilizați secțiuni critice, trebuie avut grijă să nu alocați fragmente de cod prea mari în secțiune, deoarece acest lucru poate duce la întârzieri semnificative în execuția altor fire.

De exemplu, în raport cu heap-urile deja luate în considerare, nu are sens să protejați toate funcțiile heap cu o secțiune critică, deoarece funcțiile de citire pot fi executate în paralel. Mai mult decât atât, utilizarea unei secțiuni critice chiar și pentru sincronizarea scriitorilor pare de fapt a fi incomod - deoarece pentru a sincroniza un scriitor cu cititorii, aceștia din urmă vor trebui totuși să intre în această secțiune, ceea ce duce practic la protejarea tuturor funcțiilor de către un singur. secțiune.

Există mai multe cazuri de utilizare eficientă a secțiunilor critice:

cititorii nu intră în conflict cu scriitorii (numai scriitorii trebuie protejați);

toate firele de discuție au drepturi de acces aproximativ egale (să zicem, nu poți să evidențiezi scriitorii și cititorii puri);

la construirea de obiecte de sincronizare compuse, constând din mai multe standard, pentru a proteja operațiunile secvențiale pe un obiect compus.

În părțile anterioare ale articolului, am vorbit despre principii generaleși metode specifice pentru construirea de aplicații multi-threaded. Firele diferite aproape întotdeauna trebuie să interacționeze periodic între ele, iar nevoia de sincronizare apare inevitabil. Astăzi vom arunca o privire la cel mai important, mai puternic și versatil instrument de sincronizare Windows: Kernel Sync Objects.

WaitForMultipleObjects și alte funcții de așteptare

După cum vă amintiți, pentru a sincroniza firele, de obicei trebuie să suspendați temporar execuția unuia dintre fire. Cu toate acestea, trebuie tradus prin mijloace sistem de operareîntr-o stare de așteptare în care nu ocupă timp CPU. Știm deja două funcții care pot face acest lucru: SuspendThread și ResumeThread . Dar, așa cum am spus în partea anterioară a articolului, din cauza unor caracteristici, aceste funcții nu sunt potrivite pentru sincronizare.

Astăzi ne vom uita la o altă funcție care pune și firul în starea de așteptare, dar spre deosebire de SuspendThread/ResumeThread , este conceput special pentru organizarea sincronizării. Este WaitForMultipleObjects. Deoarece această caracteristică este atât de importantă, mă voi abate puțin de la regula mea de a nu intra în detaliile API-ului și voi vorbi despre ea mai detaliat, chiar și prototipul său:

DWORD WaitForMultipleObjects (

DWORD nCount , // numărul de obiecte din matricea lpHandles

MÂNER CONST * lpManere , // pointer către o matrice de descriptori de obiecte kernel

BOOL bWaitAll , // steag care indică dacă să aștepte toate obiectele sau doar unul este suficient

DWORD dwMilisecunde // pauză

Parametrul principal al acestei funcții este un pointer către o matrice de mânere de obiecte ale nucleului. Vom vorbi mai jos despre ce sunt aceste obiecte. Deocamdată, este important pentru noi să știm că oricare dintre aceste obiecte se poate afla în una dintre cele două stări: neutru sau „de semnalizare” (stare semnalată). Dacă indicatorul bWaitAll este FALSE, funcția va reveni imediat ce cel puțin unul dintre obiecte dă un semnal. Și dacă steag-ul este TRUE, acest lucru se va întâmpla numai atunci când toate obiectele încep să semnalizeze simultan (după cum vom vedea, aceasta este cea mai importantă proprietate a acestei funcții). În primul caz, prin valoarea returnată, puteți afla care dintre obiecte a dat semnalul. Trebuie să scădeți constanta WAIT_OBJECT_0 din ea și obțineți un index în tabloul lpHandles. Dacă timeout-ul depășește timeout-ul specificat în ultimul parametru, funcția va opri așteptarea și va returna valoarea WAIT_TIMEOUT . Ca un timeout, puteți specifica constanta INFINITE , iar apoi funcția va aștepta „până când se oprește”, sau puteți invers 0, iar apoi firul nu va fi suspendat deloc. În acest din urmă caz, funcția va reveni imediat, dar rezultatul ei vă va spune starea obiectelor. Ultima tehnică este folosită foarte des. După cum puteți vedea, această funcție are caracteristici bogate. Există mai multe alte funcții WaitForXXX, dar toate sunt variații ale temei principale. În special, WaitForSingleObject este doar o versiune simplificată a acestuia. Restul are fiecare funcționalitate suplimentară, dar sunt folosite, în general, mai rar. De exemplu, ele fac posibil să se răspundă nu numai la semnalele de la obiectele nucleului, ci și la sosirea de noi mesaje ferestre în coada firului de execuție. Descrierea lor, precum și informații detaliate despre WaitForMultipleObjects, le veți găsi, ca de obicei, în MSDN.

Acum despre ce sunt aceste misterioase „obiecte nucleu”. Pentru început, acestea includ firele și procesele în sine. Ele intră în starea de semnalizare imediat după finalizare. Aceasta este o caracteristică foarte importantă, deoarece este adesea necesar să țineți evidența când s-a încheiat un fir sau un proces. De exemplu, aplicația noastră de server cu un set de fire de lucru ar trebui să fie finalizată. În același timp, firul de control trebuie să informeze firele de lucru într-un fel că este timpul să termine lucrul (de exemplu, prin setarea unui steag global), apoi să aștepte până când toate firele de execuție s-au finalizat, făcând tot ce este necesar pentru finalizarea corectă. a acțiunii: eliberarea resurselor, informarea clienților despre închidere, închiderea conexiunilor la rețea etc.

Faptul că firele pornesc un semnal la sfârșitul lucrării face extrem de ușor de rezolvat problema sincronizării cu terminarea firului:

// Pentru simplitate, să avem doar un fir de lucru. Hai să-l rulăm:

MANERA hWorkerThread = :: Create Thread (...);

// Înainte de sfârșitul lucrării, trebuie să spunem cumva firului de lucru lucrător că este timpul să încărcăm.

// Așteptați ca firul să se termine:

DWORD dwWaitResult = :: WaitForSingleObject ( hWorkerThread , INFINIT );

dacă( dwWaitResult != WAIT_OBJECT_0 ) { /* eroare de manipulare */ }

// „Manerul” fluxului poate fi închis:

VERIFICA (:: CloseHandle ( hWorkerThread );

/* Dacă CloseHandle eșuează și returnează FALSE, nu fac o excepție. În primul rând, chiar dacă acest lucru s-a întâmplat din cauza unei erori de sistem, nu ar avea consecințe directe pentru programul nostru, deoarece, deoarece închidem mânerul, nu se așteaptă nicio lucrare cu acesta în viitor. În realitate, eșecul CloseHandle poate însemna doar o eroare în programul tău. Prin urmare, vom insera macro-ul VERIFY aici pentru a nu o rata în etapa de depanare a aplicației. */

Codul care așteaptă încheierea procesului va arăta similar.

Dacă nu ar exista o astfel de capacitate încorporată, firul de execuție ar trebui să transmită cumva informații despre finalizarea sa firului principal în sine. Chiar dacă ar fi făcut acest lucru în ultimul rând, firul principal nu ar putea fi sigur că lucrătorul nu mai are cel puțin câteva instrucțiuni de asamblare rămase de executat. LA situatii individuale(de exemplu, dacă codul firului de execuție este într-un DLL care trebuie descărcat când se termină) acest lucru poate fi fatal.

Vreau să vă reamintesc că, chiar și după ce un fir de execuție (sau un proces) s-a încheiat, mânerele sale rămân în vigoare până când sunt închise în mod explicit de către funcția CloseHandle. (Apropo, nu uitați să faceți acest lucru!) Acest lucru se face doar pentru ca în orice moment să puteți verifica starea firului.

Deci, funcția WaitForMultipleObjects (și analogii săi) vă permite să sincronizați execuția unui fir cu starea obiectelor de sincronizare, în special, alte fire și procese.

Obiecte speciale Kernel

Să trecem la luarea în considerare a obiectelor kernel, care sunt concepute special pentru sincronizare. Acestea sunt evenimente, semafore și mutexuri. Să aruncăm o privire pe scurt la fiecare dintre ele:

eveniment

Poate cel mai simplu și fundamental obiect de sincronizare. Acesta este doar un steag care poate fi setat cu funcțiile SetEvent / ResetEvent: semnalizare sau neutru. Un eveniment este cel mai convenabil mod de a semnala unui fir în așteptare că a avut loc un eveniment (de aceea se numește) și puteți continua să lucrați. Folosind un eveniment, putem rezolva cu ușurință problema de sincronizare la inițializarea unui fir de lucru:

// Să păstrăm mânerul evenimentului într-o variabilă globală pentru simplitate:

HANDLE g_hEventInitComplete = NUL ; // nu lăsați niciodată o variabilă neinițializată!

{ // cod în firul principal

// creează un eveniment

g_hEventInitComplete = :: Creează eveniment ( NUL,

FALS , // vom vorbi despre acest parametru mai târziu

FALS , // starea inițială - neutră

dacă(! g_hEventInitComplete ) { /* Nu uitați de tratarea erorilor */ }

// creează un fir de lucru

DWORD idWorkerThread = 0 ;

MANERA hWorkerThread = :: Create Thread ( NUL , 0 , & WorkerThreadProc , NUL , 0 , & idWorkerThread );

dacă(! hWorkerThread ) { /* eroare de manipulare */ }

// așteptați un semnal de la firul de lucru

DWORD dwWaitResult = :: WaitForSingleObject ( g_hEventInitComplete , INFINIT );

dacă( dwWaitResult != WAIT_OBJECT_0 ) { /* eroare */ }

// acum puteți fi sigur că firul de lucru a finalizat inițializarea.

VERIFICA (:: CloseHandle ( g_hEventInitComplete )); // nu uitați să închideți obiectele inutile

g_hEventInitComplete = NUL ;

// funcția de flux de lucru

DWORD WINAPI WorkerThreadProc ( LPVOID_parameter )

InitializeWorker (); // inițializare

// semnalează că inițializarea este completă

BOOL este OK = :: SetEvent ( g_hEventInitComplete );

dacă(! este in regula ) { /* eroare */ }

Trebuie remarcat faptul că există două varietăți semnificativ diferite de evenimente. Putem selecta unul dintre ele folosind al doilea parametru al funcției CreateEvent. Dacă este TRUE, este creat un eveniment a cărui stare este controlată doar manual, adică de către funcțiile SetEvent/ResetEvent. Dacă este FALS, va fi generat un eveniment de resetare automată. Aceasta înseamnă că de îndată ce un fir care așteaptă un anumit eveniment este eliberat de un semnal de la acest eveniment, acesta va fi resetat automat la o stare neutră. Diferența lor este cea mai pronunțată într-o situație în care mai multe fire așteaptă un eveniment deodată. Un eveniment controlat manual este ca un pistol de pornire. De îndată ce este setat la starea semnalată, toate firele vor fi eliberate odată. Un eveniment de resetare automată, pe de altă parte, este ca un turnichet de metrou: va elibera un singur flux și va reveni la o stare neutră.

Mutex

În comparație cu un eveniment, acesta este un obiect mai specializat. Este de obicei folosit pentru a rezolva o problemă comună de sincronizare, cum ar fi accesarea unei resurse partajate de mai multe fire. În multe privințe, este similar cu un eveniment de resetare automată. Principala diferență este că are o legare specială la un anumit fir. Dacă mutex-ul este în starea semnalată, înseamnă că este liber și nu aparține niciunui thread. De îndată ce un anumit thread a așteptat acest mutex, acesta din urmă este resetat la o stare neutră (aici este la fel ca un eveniment de resetare automată), iar firul de execuție devine proprietarul său până când eliberează în mod explicit mutex-ul cu funcția ReleaseMutex, sau se încheie. Astfel, pentru a fi siguri că numai un fir de execuție funcționează cu date partajate la un moment dat, toate locurile în care au loc astfel de lucrări ar trebui să fie înconjurate de o pereche: WaitFor - ReleaseMutex :

MANEREA g_hMutex ;

// Lăsați mânerul mutex să fie stocat într-o variabilă globală. Desigur, trebuie creat în prealabil, înainte de începerea firelor de lucru. Să presupunem că acest lucru a fost deja făcut.

int eu astept = :: WaitForSingleObject ( g_hMutex , INFINIT );

intrerupator( eu astept ) {

caz WAIT_OBJECT_0 : // Totul e bine

pauză;

caz WAIT_ABANDONED : /* Un fir s-a încheiat, uitând să apelăm ReleaseMutex. Cel mai probabil, asta înseamnă o eroare în programul tău! Prin urmare, pentru orice eventualitate, vom insera ASSERT aici, dar în versiunea finală (lansare) vom considera acest cod ca fiind de succes. */

AFIRMA ( fals );

pauză;

Mod implicit:

// Tratarea erorilor ar trebui să fie aici.

// O bucată de cod protejată de mutex.

ProcessCommonData ();

VERIFICA (:: Eliberați Mutex ( g_hMutex ));

De ce este un mutex mai bun decât un eveniment de resetare automată? În exemplul de mai sus, ar putea fi folosit și, numai ReleaseMutex ar trebui înlocuit cu SetEvent . Cu toate acestea, poate apărea următoarea dificultate. Cel mai adesea, trebuie să lucrați cu date partajate în mai multe locuri. Ce se întâmplă dacă ProcessCommonData din exemplul nostru apelează o funcție care funcționează cu aceleași date și care are deja propria sa pereche de WaitFor - ReleaseMutex (în practică, acest lucru este foarte comun)? Dacă ar fi să folosim un eveniment, programul s-ar bloca evident, deoarece în interiorul blocului protejat, evenimentul este într-o stare neutră. Mutexul este mai complicat. Rămâne întotdeauna în starea de semnalizare pentru firul principal, chiar dacă este în stare neutră pentru toate celelalte fire. Prin urmare, dacă un fir de execuție a dobândit mutex-ul, apelarea din nou a funcției WaitFor nu va bloca. Mai mult, un contor este, de asemenea, încorporat în mutex, așa că ReleaseMutex trebuie apelat de același număr de ori cât au existat apeluri la WaitFor . Astfel, putem proteja în siguranță fiecare bucată de cod care funcționează cu date partajate cu o pereche WaitFor - ReleaseMute x, fără a ne îngrijora că acest cod poate fi apelat recursiv. Acest lucru face ca mutexul să fie un instrument foarte ușor de utilizat.

Semafor

Un obiect de sincronizare și mai specific. Trebuie să mărturisesc că în practica mea nu a existat încă un caz în care ar fi util. Un semafor este conceput pentru a limita numărul maxim de fire care pot lucra pe o resursă în același timp. În esență, un semafor este un eveniment cu un contor. Atâta timp cât acest contor este mai mare decât zero, semaforul este în starea de semnalizare. Cu toate acestea, fiecare apel la WaitFor scade acest contor cu unu până când devine zero și semaforul intră în stare neutră. La fel ca un mutex, un semafor are o funcție ReleaseSemaphor care crește un contor. Totuși, spre deosebire de un mutex, un semafor nu este legat de fire, iar apelarea WaitFor/ReleaseSemaphor din nou va decrementa/incrementa contorul.

Cum poate fi folosit un semafor? De exemplu, poate fi folosit pentru a restricționa artificial multithreading. După cum am menționat deja, prea multe fire de execuție active simultan pot degrada considerabil performanța întregului sistem din cauza schimbărilor frecvente de context. Și dacă ar trebui să creăm prea multe fire de lucru, putem limita numărul de fire de execuție active simultan la un număr de ordinea numărului de procesoare.

Ce se mai poate spune despre obiectele de sincronizare a nucleului? Este foarte convenabil să le dai nume. Toate funcțiile care creează obiecte de sincronizare au parametrul corespunzător: CreateEvent , CreateMutex , CreateSemaphore . Dacă apelați, de exemplu, CreateEvent de două ori, de ambele ori specificând același nume nevid, atunci a doua oară funcția, în loc să creeze un nou obiect, va returna mânerul unuia existent. Acest lucru se va întâmpla chiar dacă al doilea apel a fost efectuat dintr-un alt proces. Acesta din urmă este foarte convenabil în cazurile în care doriți să sincronizați fire aparținând unor procese diferite.

Când nu mai aveți nevoie de obiectul de sincronizare, nu uitați să apelați funcția CloseHandle pe care am menționat-o mai devreme când vorbeam despre fire. De fapt, nu va șterge neapărat obiectul imediat. Ideea este că un obiect poate avea mai multe mânere, iar apoi va fi șters doar când ultimul este închis.

Vreau să vă reamintesc că Cel mai bun mod pentru a vă asigura că CloseHandle sau o funcție similară de „curățare” este sigur că va fi apelată, chiar și în cazul unei situații anormale, este să o puneți într-un destructor. Apropo, acest lucru a fost odată descris bine și în detaliu în articolul lui Kirill Pleshivtsev „Smart Destructor”. În exemplele de mai sus, nu am folosit această tehnică doar în scopuri educaționale, astfel încât munca funcțiilor API a fost mai vizuală. În codul real, ar trebui să utilizați întotdeauna clase de wrapper cu destructori inteligenți pentru curățare.

Apropo, cu funcția ReleaseMutex și altele asemenea, apare în mod constant aceeași problemă ca și cu CloseHandle . Trebuie apelat la sfârșitul lucrului cu date partajate, indiferent de cât de succes a fost finalizată această lucrare (la urma urmei, s-ar putea face o excepție). Consecințele „uitării” sunt mai grave aici. Dacă nu este numit CloseHandle va scurge doar resurse (ceea ce este de asemenea rău!), atunci un mutex nelansat va împiedica alte fire de execuție să lucreze cu resursa partajată până la terminarea firului de execuție eșuat, ceea ce cel mai probabil nu va permite aplicației să funcționeze normal. Pentru a evita acest lucru, o clasă special instruită cu un distrugător inteligent ne va ajuta din nou.

Terminând revizuirea obiectelor de sincronizare, aș dori să menționez un obiect care nu se află în API-ul Win32. Mulți dintre colegii mei se întreabă de ce Win32 nu are un obiect specializat „unul scrie, multe citește”. Un fel de „mutex avansat”, care se asigură că doar un fir poate accesa simultan datele partajate pentru scriere, iar mai multe fire pot să citească simultan. Un obiect similar poate fi găsit în UNIX "ah. Unele biblioteci, de exemplu din Borland, oferă să-l emuleze pe baza unor obiecte de sincronizare standard. Cu toate acestea, beneficiul real al unor astfel de emulări este foarte îndoielnic. Un astfel de obiect poate fi implementat eficient doar la nivelul nucleului sistemului de operare.Dar în nucleul Windows nu oferă un astfel de obiect.

De ce nu s-au ocupat dezvoltatorii nucleului Windows NT de asta? De ce suntem mai răi decât UNIX? În opinia mea, răspunsul este că pur și simplu nu a existat încă o nevoie reală pentru un astfel de obiect pentru Windows. Pe o mașină obișnuită cu uniprocesor, unde firele de execuție încă nu pot funcționa fizic simultan, va fi practic echivalent cu un mutex. Pe o mașină cu multiprocesor, poate beneficia permițând firelor de execuție să ruleze în paralel. În același timp, acest câștig va deveni tangibil numai atunci când probabilitatea unei „coliziune” a firelor de citire este mare. Fără îndoială, de exemplu, pe o mașină cu procesor 1024, un astfel de obiect nucleu va fi vital. Există mașini similare, dar sunt sisteme specializate care rulează sisteme de operare specializate. Adesea, astfel de sisteme de operare sunt construite pe baza UNIX, probabil de acolo un obiect precum „unul scrie, mulți citesc” a intrat în versiunile mai frecvent utilizate ale acestui sistem. Dar pe mașinile x86 suntem obișnuiți, de regulă, să fie instalate doar unul și doar ocazional două procesoare. Și doar cele mai avansate modele de procesoare precum Intel Xeon suportă 4 sau chiar mai multe configurații de procesor, dar astfel de sisteme rămân totuși exotice. Dar chiar și pe un astfel de sistem „avansat”, un „mutex avansat” poate oferi un câștig de performanță vizibil doar în situații foarte specifice.

Astfel, implementarea unui mutex „avansat” pur și simplu nu merită problemele. Pe o mașină cu „procesor scăzut”, poate fi chiar mai puțin eficientă din cauza complexității logicii obiectului în comparație cu un mutex standard. Vă rugăm să rețineți că implementarea unui astfel de obiect nu este atât de simplă pe cât ar părea la prima vedere. Cu o implementare nereușită, dacă există prea multe fire de citire, firul de scriere pur și simplu „nu ajunge” la date. Din aceste motive, nici nu vă recomand să încercați să emulați un astfel de obiect. În aplicațiile reale pe mașini reale, un mutex obișnuit sau secțiune critică (care va fi discutată în următoarea parte a articolului) va face față perfect sarcinii de sincronizare a accesului la datele partajate. Deși, presupun, odată cu dezvoltarea sistemului de operare Windows, obiectul kernel „unul scrie, multe citește” va apărea mai devreme sau mai târziu.

Notă. De fapt, obiectul „unul scrie - mulți citesc” în Windows NT încă există. Doar că nu știam despre asta când am scris acest articol. Acest obiect se numește „resurse kernel” și nu este accesibil programelor în modul utilizator, motiv pentru care probabil nu este bine cunoscut. Asemănări despre acesta pot fi găsite în DDK. Mulțumesc lui Konstantin Manurin pentru că mi-a subliniat acest lucru.

Impas

Acum să revenim la funcția WaitForMultipleObjects, mai precis, la al treilea parametru, bWaitAll. Am promis să vă spun de ce este atât de importantă capacitatea de a aștepta mai multe obiecte deodată.

De ce este necesară o funcție pentru a aștepta unul dintre mai multe obiecte este de înțeles. În absența unei funcții speciale, acest lucru ar putea fi făcut, cu excepția verificării secvențiale a stării obiectelor într-o buclă goală, ceea ce, desigur, este inacceptabil. Dar necesitatea unei funcții speciale care să vă permită să așteptați momentul în care mai multe obiecte intră în starea semnalului deodată nu este atât de evidentă. Într-adevăr, imaginați-vă următoarea situație tipică: la un moment dat, thread-ul nostru are nevoie de acces la două seturi de date partajate simultan, fiecare dintre ele fiind responsabil pentru propriul mutex, să le numim A și B. S-ar părea că firul poate mai întâi așteptați până când mutexul A este eliberat, capturați-l, apoi așteptați ca mutexul B să fie eliberat... Se pare că putem face cu câteva apeluri la WaitForSingleObject. Într-adevăr, acest lucru va funcționa, dar numai atâta timp cât toate celelalte fire dobândesc mutexurile în aceeași ordine: mai întâi A, apoi B. Ce se întâmplă dacă un anumit thread încearcă să facă opusul: mai întâi achiziționează B, apoi A? Mai devreme sau mai târziu, va apărea o situație când un fir a capturat mutexul A, altul B, primul așteaptă să fie eliberat B, al doilea A. Este clar că nu vor aștepta niciodată acest lucru și programul se va bloca.

Acest tip de blocaj este o eroare foarte comună. Ca toate erorile legate de sincronizare, apare doar din când în când și poate strica o mulțime de nervi pentru un programator. În același timp, aproape orice schemă care implică mai multe obiecte de sincronizare este plină de blocaj. Prin urmare, acestei probleme ar trebui să i se acorde o atenție deosebită în etapa de proiectare a unui astfel de circuit.

În exemplul simplu dat, blocarea este destul de ușor de evitat. Este necesar să se ceară ca toate firele să obțină mutexuri într-o anumită ordine: mai întâi A, apoi B. Cu toate acestea, într-un program complex, în care există multe obiecte legate între ele în diferite moduri, acest lucru nu este de obicei atât de ușor de realizat. Nu două, dar multe obiecte și fire pot fi implicate într-o lacăt. Prin urmare, cel mai mult mod de încredere Pentru a evita blocajul într-o situație în care un fir de execuție are nevoie de mai multe obiecte de sincronizare simultan, este să le capturați pe toate cu un apel la funcția WaitForMultipleObjects cu parametrul bWaitAll=TRUE. Pentru a spune adevărul, în acest caz, trecem doar problema blocajelor la nucleul sistemului de operare, dar principalul lucru este că aceasta nu va mai fi preocuparea noastră. Cu toate acestea, într-un program complex cu multe obiecte, când nu este întotdeauna posibil să spunem imediat care dintre ele va fi necesar pentru a efectua o anumită operație, adesea nu este ușor să aduceți toate apelurile WaitFor într-un singur loc și să le combinați și pe acestea.

Astfel, există două moduri de a evita blocajul. Trebuie fie să vă asigurați că obiectele de sincronizare sunt întotdeauna capturate de fire de execuție în exact aceeași ordine, fie că sunt capturate printr-un singur apel la WaitForMultipleObjects . Această din urmă metodă este mai simplă și preferată. Cu toate acestea, în practică, odată cu îndeplinirea ambelor cerințe, apar în mod constant dificultăți, este necesară combinarea ambelor abordări. Proiectarea circuitelor de sincronizare complexe este adesea o sarcină extrem de netrivială.

Exemplu de sincronizare

În majoritatea situațiilor tipice, precum cele pe care le-am descris mai sus, nu este dificil să organizezi sincronizarea, un eveniment sau un mutex este suficient. Dar periodic există cazuri mai complexe în care soluția problemei nu este atât de evidentă. Aș dori să ilustrez acest lucru cu un exemplu concret din practica mea. După cum veți vedea, soluția s-a dovedit a fi surprinzător de simplă, dar înainte de a o găsi, a trebuit să încerc mai multe opțiuni nereușite.

Deci sarcina. Aproape toți managerii moderni de descărcare, sau pur și simplu „scaunele balansoare”, au capacitatea de a restricționa traficul, astfel încât „scaunul balansoar” care rulează în fundal nu interferează foarte mult cu navigarea utilizatorului pe Web. Dezvoltam un program similar și mi s-a dat sarcina de a implementa o astfel de „funcție”. balansoarul meu a funcționat conform schemei clasice multithreading, când fiecare sarcină, în acest caz, descărcarea unui anumit fișier, este gestionată de un fir separat. Limita de trafic ar fi trebuit să fie cumulativă pentru toate fluxurile. Adică, era necesar să se asigure că, într-un anumit interval de timp, toate fluxurile citesc din socket-urile lor nu mai mult de un anumit număr de octeți. Simpla împărțire a acestei limite în mod egal între fluxuri va fi evident ineficientă, deoarece descărcarea fișierelor poate fi foarte neuniformă, unul se va descărca rapid, celălalt încet. Prin urmare, avem nevoie de un contor comun pentru toate firele de execuție, câți octeți au fost citiți și câți mai pot fi citiți. Aici este utilă sincronizarea. O complexitate suplimentară a sarcinii a fost dată de cerința ca în orice moment oricare dintre firele de lucru să poată fi oprite.

Să formulăm problema mai detaliat. Am decis să încadrez sistemul de sincronizare într-o clasă specială. Iată interfața sa:

clasă CCota {

public: // metode

gol a stabilit ( nesemnat int _nCota );

nesemnat int Cerere ( nesemnat int _nBytesToRead , HANDLE_hStopEvent );

gol Eliberare ( nesemnat int _nBytesRevert , HANDLE_hStopEvent );

Periodic, să spunem o dată pe secundă, firul de control apelează metoda Set, stabilind cota de descărcare. Înainte ca thread-ul de lucru să citească datele primite din rețea, apelează metoda Request, care verifică dacă cota curentă nu este zero și, dacă da, returnează numărul de octeți care pot fi citiți mai puțin decât cota curentă. Cota este redusă în mod corespunzător cu acest număr. Dacă cota este zero atunci când este apelată Solicitarea, firul care apelează trebuie să aștepte până când cota este disponibilă. Uneori se întâmplă să se primească efectiv mai puțini octeți decât cei solicitați, caz în care firul de execuție returnează o parte din cota alocată acestuia prin metoda Release. Și, după cum am spus, utilizatorul poate oricând da comanda pentru a opri descărcarea. În acest caz, așteptarea trebuie întreruptă, indiferent de prezența unei cote. Pentru aceasta este folosit un eveniment special: _hStopEvent. Deoarece sarcinile pot fi pornite și oprite independent, fiecare fir de lucru are propriul eveniment de oprire. Mânerul său este trecut la metodele Request și Release.

Într-una dintre opțiunile nereușite, am încercat să folosesc o combinație de un mutex care sincronizează accesul la clasa CQuota și un eveniment care semnalează prezența unei cote. Cu toate acestea, evenimentul stop nu se încadrează în această schemă. Dacă un fir de execuție dorește să obțină o cotă, atunci starea sa de așteptare trebuie controlată de o expresie booleană complexă: ((eveniment mutex ȘI cotă) SAU eveniment stop). Dar WaitForMultipleObjects nu permite acest lucru, puteți combina mai multe obiecte kernel fie cu o operație AND sau OR, dar nu amestecate. Încercarea de a împărți așteptarea cu două apeluri consecutive către WaitForMultipleObjects duce inevitabil la un impas. În general, această cale s-a dovedit a fi o fundătură.

Nu voi mai lăsa ceața să intre și vă spun soluția. După cum am spus, un mutex este foarte asemănător cu un eveniment de resetare automată. Și aici avem doar acel caz rar în care este mai convenabil să îl folosiți, dar nu unul, ci două deodată:

clasă CCota {

privat: // date

nesemnat int m_nCota ;

CEvent m_eventHasQuota ;

CEvent m_eventNoQuota ;

Doar unul dintre aceste evenimente poate fi setat la un moment dat. Orice fir care manipulează cota trebuie să seteze primul eveniment dacă cota rămasă este diferită de zero, iar al doilea dacă cota a fost epuizată. Un fir care dorește să obțină o cotă trebuie să aștepte primul eveniment. Firul de creștere a cotei trebuie doar să aștepte oricare dintre aceste evenimente, deoarece dacă ambele sunt în starea de resetare, înseamnă că un alt fir lucrează în prezent cu cota. Astfel, două evenimente îndeplinesc două funcții simultan: sincronizarea accesului la date și așteptarea. În cele din urmă, deoarece thread-ul așteaptă unul dintre cele două evenimente, evenimentul care semnalează oprirea este ușor de inclus.

Voi da un exemplu de implementare a metodei Request. Restul sunt implementate într-un mod similar. Am simplificat ușor codul folosit în proiectul real:

nesemnat int CCota :: Cerere ( nesemnat int _nSolicitare , HANDLE_hStopEvent )

dacă(! _nSolicitare ) întoarcere 0 ;

nesemnat int n Furnizați = 0 ;

MANIPULAți evenimentele [ 2 ];

hEvenimente [ 0 ] = _hStopEvent ; // Evenimentul de oprire are prioritate mai mare. O punem pe primul loc.

hEvenimente [ 1 ] = m_eventHasQuota ;

int iWaitResult = :: WaitForMultipleObjects ( 2 , hEvenimente , FALS , INFINIT );

intrerupator( iWaitResult ) {

caz WAIT_FAILED :

// EROARE

arunca noua CWin32Exception ;

caz WAIT_OBJECT_0 :

// Opriți evenimentul. L-am gestionat cu o excepție personalizată, dar nimic nu mă împiedică să o implementez în alt mod.

arunca noua CStopException ;

caz WAIT_OBJECT_0 + 1 :

// Eveniment „cota disponibilă”

AFIRMA ( m_nCota ); // Dacă semnalul a fost dat de acest eveniment, dar de fapt nu există nicio cotă, atunci undeva am făcut o greșeală. Trebuie să caut bug-ul!

dacă( _nSolicitare >= m_nCota ) {

n Furnizați = m_nCota ;

m_nCota = 0 ;

m_eventNoQuota . a stabilit ();

altfel {

n Furnizați = _nSolicitare ;

m_nCota -= _nSolicitare ;

m_eventHasQuota . a stabilit ();

pauză;

întoarcere n Furnizați ;

O mică notă. Biblioteca MFC nu a fost folosită în acel proiect, dar, după cum probabil ați ghicit deja, mi-am creat propria mea clasă CEvent, un wrapper în jurul obiectului kernel „eveniment”, similar cu MFC „schnoy. După cum am spus, clase de wrapper atât de simple. sunt foarte utile atunci când există o resursă (în acest caz, un obiect kernel) care trebuie reținut să fie eliberată la sfârșitul lucrării. În rest, nu contează dacă scrieți SetEvent(m_hEvent) sau m_event.Set( ).

Sper că acest exemplu vă va ajuta să vă proiectați propria schemă de sincronizare dacă vă confruntați cu o situație non-trivială. Principalul lucru este să vă analizați schema cât mai atent posibil. Ar putea exista o situație în care nu ar funcționa corect, în special, ar putea apărea blocarea? Prinderea unor astfel de erori în depanator este de obicei o afacere fără speranță, doar o analiză detaliată ajută aici.

Deci ne-am gândit instrument esențial sincronizare fir: obiecte de sincronizare a nucleului. Este un instrument puternic și versatil. Cu el, puteți construi chiar și scheme de sincronizare foarte complexe. Din fericire, astfel de situații non-triviale sunt rare. În plus, versatilitatea vine întotdeauna cu prețul performanței. Prin urmare, în multe cazuri, merită să utilizați celelalte funcții de sincronizare a firelor disponibile în Windows, cum ar fi secțiunile critice și operațiunile atomice. Nu sunt atât de universale, dar sunt simple și eficiente. Despre ele vom vorbi în partea următoare.

Un proces este o instanță a unui program încărcat în memorie. Această instanță poate crea fire de execuție, care sunt o secvență de instrucțiuni care trebuie executate. Este important să înțelegeți că nu procesele rulează, ci firele de execuție.

Mai mult, orice proces are cel puțin un fir. Acest thread se numește firul principal (principal) al aplicației.

Deoarece există aproape întotdeauna mult mai multe fire de execuție decât procesoare fizice pentru executarea lor, firele de execuție sunt de fapt executate nu simultan, ci la rândul lor (distribuția timpului procesorului are loc tocmai între fire). Dar comutarea între ele se întâmplă atât de des, încât pare că rulează în paralel.

În funcție de situație, firele pot fi în trei stări. În primul rând, un fir de execuție poate rula atunci când i se oferă timp CPU, de exemplu. poate fi activ. În al doilea rând, poate fi inactiv și în așteptare ca un procesor să fie alocat, adică. fi într-o stare de pregătire. Și există un al treilea, de asemenea foarte condiție importantă- stare de blocare. Când un fir este blocat, nu i se alocă deloc timp. De obicei, un lacăt este plasat în așteptarea unui eveniment. Când are loc acest eveniment, firul de execuție trece automat de la starea blocată la starea gata. De exemplu, dacă un fir efectuează calcule în timp ce altul trebuie să aștepte ca rezultatele să fie salvate pe disc. Al doilea ar putea folosi o buclă de genul „while(!isCalcFinished) continue;”, dar este ușor de observat în practică că procesorul este 100% ocupat în timp ce această buclă rulează (aceasta se numește așteptare activă). Astfel de bucle ar trebui evitate ori de câte ori este posibil, în care mecanismul de blocare oferă o asistență neprețuită. Al doilea thread se poate bloca până când primul fir setează un eveniment pentru a semnala că citirea s-a terminat.

Sincronizarea firelor în sistemul de operare Windows

Windows implementează multitasking preventiv, ceea ce înseamnă că în orice moment sistemul poate întrerupe execuția unui fir și poate transfera controlul către altul. Anterior, în Windows 3.1, se folosea o metodă de organizare numită cooperative multitasking: sistemul aștepta până când firul însuși îi transferă controlul și, de aceea, dacă o aplicație se blochează, computerul trebuia repornit.

Toate firele de execuție care aparțin aceluiași proces au anumite resurse comune, cum ar fi spațiul de adresă RAM sau fișierele deschise. Aceste resurse aparțin întregului proces și, prin urmare, fiecăruia dintre firele sale. Prin urmare, fiecare fir poate funcționa cu aceste resurse fără nicio restricție. Dar... Dacă un fir de execuție nu a terminat încă de lucrat cu nicio resursă partajată și sistemul a trecut la un alt fir folosind aceeași resursă, atunci rezultatul muncii acestor fire poate fi extrem de diferit de ceea ce a fost intenționat. Astfel de conflicte pot apărea și între firele care aparțin unor procese diferite. Ori de câte ori două sau mai multe fire folosesc un fel de resursă partajată, apare această problemă.

Exemplu. Fire nesincronizate: dacă suspendați temporar firul de afișare (pauză), firul de umplere a matricei de fundal va continua să ruleze.

#include #include int a; MÂNER hThr; uThrID lung nesemnat; void Thread(void* pParams) ( int i, num = 0; while (1) ( for (i=0; i<5; i++) a[i] = num; num++; } } int main(void) { hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) printf("%d %d %d %d %d\n", a, a, a, a, a); return 0; }

De aceea este nevoie de un mecanism care să permită firelor de execuție să-și coordoneze munca cu resursele partajate. Acest mecanism se numește mecanism de sincronizare a firelor.

Acest mecanism este un set de obiecte ale sistemului de operare care sunt create și gestionate de software, sunt comune tuturor thread-urilor din sistem (unele sunt partajate de fire aparținând aceluiași proces) și sunt folosite pentru a coordona accesul la resurse. Resursele pot fi orice lucru care poate fi partajat de două sau mai multe fire de execuție - un fișier de pe disc, un port, o intrare în baza de date, un obiect GDI și chiar o variabilă globală de program (care poate fi accesată din firele care aparțin aceluiași proces).

Există mai multe obiecte de sincronizare, dintre care cele mai importante sunt mutex, secțiune critică, eveniment și semafor. Fiecare dintre aceste obiecte implementează propria sa metodă de sincronizare. De asemenea, procesele și firele de execuție în sine pot fi folosite ca obiecte de sincronizare (când un fir de execuție așteaptă finalizarea unui alt fir sau proces); precum și fișiere, dispozitive de comunicare, intrare în consolă și notificări de modificare.

Orice obiect de sincronizare poate fi în așa-numita stare semnalizată. Pentru fiecare tip de obiect, această stare are un sens diferit. Thread-urile pot verifica starea curentă a unui obiect și/sau aștepta ca acea stare să se schimbe și, astfel, să își coordoneze acțiunile. Acest lucru asigură că atunci când un fir lucrează cu obiecte de sincronizare (le creează, își schimbă starea), sistemul nu își va întrerupe execuția până când nu încheie această acțiune. Astfel, toate operațiile finale asupra obiectelor de sincronizare sunt atomice (indivizibile.

Lucrul cu obiecte de sincronizare

Pentru a crea unul sau altul obiect de sincronizare, se numește o funcție WinAPI specială de tip Create... (de ex. CreateMutex). Acest apel returnează un mâner de obiect (HANDLE) care poate fi folosit de toate firele de execuție aparținând procesului dat. Este posibil să accesați obiectul de sincronizare dintr-un alt proces, fie prin moștenirea mânerului obiectului, fie, de preferință, apelând funcția Open... a obiectului. După acest apel, procesul va primi un handle, care poate fi folosit ulterior pentru a lucra cu obiectul. Un obiect, cu excepția cazului în care este destinat să fie utilizat într-un singur proces, trebuie să primească un nume. Numele tuturor obiectelor trebuie să fie diferite (chiar dacă sunt de tipuri diferite). Nu puteți, de exemplu, să creați un eveniment și un semafor cu același nume.

Prin descriptorul disponibil al unui obiect, puteți determina starea lui curentă. Acest lucru se face cu ajutorul așa-numitului. funcții în așteptare. Funcția cel mai frecvent utilizată este WaitForSingleObject. Această funcție ia doi parametri, primul este mânerul obiectului, al doilea este timeout-ul în ms. Funcția returnează WAIT_OBJECT_0 dacă obiectul este în starea semnalată, WAIT_TIMEOUT dacă expirarea a expirat și WAIT_ABANDONED dacă mutex-ul nu a fost eliberat înainte de terminarea firului proprietar. Dacă expirarea este specificată ca zero, funcția revine imediat, în caz contrar, așteaptă perioada specificată. Dacă starea obiectului devine semnalată înainte de expirarea acestui timp, funcția va returna WAIT_OBJECT_0, în caz contrar funcția va returna WAIT_TIMEOUT. Dacă constanta simbolică INFINIT este specificată ca timp, atunci funcția va aștepta la nesfârșit până când starea obiectului devine semnalată.

Este foarte important ca apelul la funcția de așteptare să blocheze firul curent, adică. în timp ce un fir este inactiv, nu i se alocă timp de procesor.

Secțiuni critice

O secțiune critică pentru obiect îl ajută pe programator să izoleze secțiunea de cod în care un fir accesează o resursă partajată și previne utilizarea concomitentă a resursei. Înainte de a utiliza resursa, firul de execuție intră în secțiunea critică (apelează funcția EnterCriticalSection). Dacă orice alt thread încearcă apoi să intre în aceeași secțiune critică, execuția sa se va întrerupe până când primul fir părăsește secțiunea cu un apel la LeaveCriticalSection. Folosit numai pentru fire într-un singur proces. Ordinea de intrare în secțiunea critică nu este definită.

Există, de asemenea, o funcție TryEnterCriticalSection care verifică dacă o secțiune critică este ocupată în prezent. Cu ajutorul său, firul în curs de așteptare a accesului la resursă nu poate fi blocat, dar efectuează câteva acțiuni utile.

Exemplu. Sincronizarea firelor folosind secțiuni critice.

#include #include CRITICAL_SECTION cs; int a; MÂNER hThr; uThrID lung nesemnat; void Thread(void* pParams) ( int i, num = 0; while (1) ( EnterCriticalSection(&cs); for (i=0; i<5; i++) a[i] = num; num++; LeaveCriticalSection(&cs); } } int main(void) { InitializeCriticalSection(&cs); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { EnterCriticalSection(&cs); printf("%d %d %d %d %d\n", a, a, a, a, a); LeaveCriticalSection(&cs); } return 0; }

Excludere mutuala

Obiectele de excludere reciprocă (mutexuri, mutex - de la MUTual EXclusion) vă permit să coordonați excluderea reciprocă a accesului la o resursă partajată. Starea semnalată a unui obiect (adică starea „setată”) corespunde momentului în care obiectul nu aparține niciunui fir și poate fi „capturat”. Dimpotrivă, starea „resetare” (nesemnalizată) corespunde momentului în care un thread deține deja acest obiect. Accesul la un obiect este acordat atunci când firul care deține obiectul îl eliberează.

Două (sau mai multe) fire pot crea un mutex cu același nume apelând funcția CreateMutex. Primul thread creează de fapt mutex-ul, iar următoarele fire primesc un mâner pentru un obiect deja existent. Acest lucru face posibil ca mai multe fire de execuție să obțină un mâner pentru același mutex, eliberând programatorul de a fi nevoit să-și facă griji cu privire la cine creează de fapt mutex-ul. Dacă se folosește această abordare, este de dorit să setați steag-ul bInitialOwner la FALSE, altfel vor exista unele dificultăți în determinarea creatorului real al mutex-ului.

Firele multiple pot dobândi un mâner la același mutex, făcând posibilă comunicarea între procese. Puteți utiliza următoarele mecanisme pentru această abordare:

  • Un proces copil creat folosind funcția CreateProcess poate moșteni mânerul mutex dacă parametrul lpMutexAttributes a fost specificat când mutex-ul a fost creat de funcția CreateMutex.
  • Un fir poate obține o copie a unui mutex existent folosind funcția DuplicateHandle.
  • Un fir poate specifica numele unui mutex existent atunci când apelează funcțiile OpenMutex sau CreateMutex.

Pentru a declara un mutex deținut de firul curent, trebuie apelată una dintre funcțiile în așteptare. Firul care deține obiectul îl poate „captura” în mod repetat de câte ori dorește (acest lucru nu va duce la autoblocare), dar va trebui să-l elibereze de câte ori folosind funcția ReleaseMutex.

Pentru a sincroniza firele unui proces, este mai eficient să folosiți secțiuni critice.

Exemplu. Sincronizarea thread-urilor folosind mutexuri.

#include #include MÂNER hMutex; int a; MÂNER hThr; uThrID lung nesemnat; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hMutex, INFINITE); for (i=0; i<5; i++) a[i] = num; num++; ReleaseMutex(hMutex); } } int main(void) { hMutex=CreateMutex(NULL, FALSE, NULL); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hMutex, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); ReleaseMutex(hMutex); } return 0; }

Evoluții

Obiectele eveniment sunt folosite pentru a notifica firele de execuție în așteptare că a avut loc un eveniment. Există două tipuri de evenimente - cu resetare manuală și automată. Resetarea manuală este efectuată de funcția ResetEvent. Evenimentele de resetare manuală sunt folosite pentru a notifica mai multe fire simultan. Când utilizați un eveniment de resetare automată, doar un fir de execuție în așteptare va primi notificarea și va continua execuția, restul va aștepta în continuare.

Funcția CreateEvent creează un obiect eveniment, SetEvent - setează evenimentul la starea semnal, ResetEvent - resetează evenimentul. Funcția PulseEvent setează evenimentul, iar după reluarea thread-urilor care așteaptă acest eveniment (toate cu resetare manuală și doar una cu una automată), îl resetează. Dacă nu există fire în așteptare, PulseEvent pur și simplu resetează evenimentul.

Exemplu. Sincronizarea thread-urilor folosind evenimente.

#include #include HANDLE hEvent1, hEvent2; int a; MÂNER hThr; uThrID lung nesemnat; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hEvent2, INFINITE); for (i=0; i<5; i++) a[i] = num; num++; SetEvent(hEvent1); } } int main(void) { hEvent1=CreateEvent(NULL, FALSE, TRUE, NULL); hEvent2=CreateEvent(NULL, FALSE, FALSE, NULL); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hEvent1, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); SetEvent(hEvent2); } return 0; }

semafoare

Un obiect semafor este de fapt un obiect mutex cu un contor. Acest obiect se permite să fie „capturat” de un anumit număr de fire. După aceea, „capturarea” va fi imposibilă până când unul dintre firele „capturate” anterior ale semaforului îl eliberează. Semaforele sunt folosite pentru a limita numărul de fire care pot accesa o resursă în același timp. În timpul inițializării, numărul maxim de fire este transferat la obiect, după fiecare „captură” contorul semaforului scade. Starea semnalului corespunde unei valori a contorului mai mare decât zero. Când contorul este zero, semaforul este considerat nesetat (resetat).

Funcția CreateSemaphore creează un obiect semafor cu o indicație a valorii sale inițiale maxime posibile, OpenSemaphore - returnează un mâner la un semafor existent, semaforul este capturat folosind funcții de așteptare, în timp ce valoarea semaforului este redusă cu unul, ReleaseSemaphore - eliberează semaforul cu o creștere a valorii semaforului cu valoarea specificată în numărul parametrului.

Exemplu. Sincronizarea firelor folosind semafore.

#include #include MÂNER hSem; int a; MÂNER hThr; uThrID lung nesemnat; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hSem, INFINITE); for (i=0; i<5; i++) a[i] = num; num++; ReleaseSemaphore(hSem, 1, NULL); } } int main(void) { hSem=CreateSemaphore(NULL, 1, 1, "MySemaphore1"); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hSem, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); ReleaseSemaphore(hSem, 1, NULL); } return 0; }

Acces protejat la variabile

Există o serie de funcții care vă permit să lucrați cu variabile globale din toate firele de execuție fără să vă faceți griji cu privire la sincronizare, deoarece. aceste funcții se ocupă de ele însele - execuția lor este atomică. Acestea sunt funcțiile InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd și InterlockedCompareExchange. De exemplu, funcția InterlockedIncrement crește atomic valoarea unei variabile de 32 de biți cu unul, ceea ce este util pentru diferite contoare.

Pentru a obține informații complete despre scopul, utilizarea și sintaxa tuturor funcțiilor API WIN32, trebuie să utilizați sistemul de ajutor MS SDK, care face parte din mediile de programare Borland Delphi sau CBuilder, precum și MSDN, care este furnizat ca parte a sistemul de programare Visual C.


Pentru programele care folosesc mai multe fire sau procese, este necesar ca toate să îndeplinească funcțiile care le sunt atribuite în secvența dorită. În mediul Windows 9x, în acest scop, se propune utilizarea mai multor mecanisme care asigură buna funcționare a thread-urilor. Aceste mecanisme sunt numite mecanisme de sincronizare. Să presupunem că dezvoltați un program în care două fire rulează în paralel. Fiecare fir accesează o variabilă globală partajată. Un fir, de fiecare dată când această variabilă este accesată, o crește, iar al doilea thread o decrește. La lucrul asincron simultan al firelor de execuție, apare inevitabil următoarea situație: - primul fir de execuție a citit valoarea unei variabile globale într-una locală; - OS îl întrerupe, deoarece cuantumul de timp al procesorului care i-a fost alocat sa încheiat și transferă controlul celui de-al doilea thread; - al doilea thread a citit și valoarea variabilei globale într-una locală, a decrementat-o ​​și a scris noua valoare înapoi; - OS transferă din nou controlul primului thread, care, neștiind nimic despre acțiunile celui de-al doilea thread, își crește variabila locală și își scrie valoarea în cea globală. Evident, modificările făcute de al doilea thread se vor pierde. Pentru a evita astfel de situații, este necesar să se separe în timp utilizarea datelor partajate. În astfel de cazuri, se folosesc mecanisme de sincronizare care asigură funcționarea corectă a mai multor fire. Instrumente de sincronizare în sistemul de operareWindows: 1) secțiunea critică (CriticSecțiune) este un obiect care aparține procesului, nu nucleului. Aceasta înseamnă că nu poate sincroniza fire de execuție din diferite procese. Există și funcții de inițializare (creare) și ștergere, intrare și ieșire dintr-o secțiune critică: creare - InitializeCriticalSection(...), ștergere - DeleteCriticalSection(...), entry - EnterCriticalSection(...), exit - LeaveCriticalSection (...). Restricții: deoarece nu este un obiect nucleu, nu este vizibil pentru alte procese, adică puteți proteja numai firele propriului proces. Secțiunea critică parsează valoarea unei variabile speciale de proces care este folosită ca indicator pentru a împiedica mai multe fire de execuție să execute o bucată de cod în același timp. Dintre obiectele de sincronizare, secțiunile critice sunt cele mai simple. 2) mutexmutabilexclude. Acesta este un obiect kernel, are un nume, ceea ce înseamnă că pot fi folosite pentru a sincroniza accesul la date partajate de la mai multe procese, mai exact, din firele diferitelor procese. Niciun alt thread nu poate dobândi un mutex care este deja deținut de unul dintre fire. Dacă un mutex protejează unele date partajate, acesta își va putea îndeplini funcția numai dacă fiecare fir verifică starea acestui mutex înainte de a accesa aceste date. Windows tratează un mutex ca pe un obiect partajat care poate fi semnalat sau resetat. Starea semnalată a mutexului indică faptul că este ocupat. Firele trebuie să analizeze în mod independent starea curentă a mutexurilor. Dacă doriți ca mutexul să fie accesat de firele din alte procese, trebuie să îi dați un nume. Funcții: CreateMutex(nume) - creație, hnd=OpenMutex(nume) - deschidere, WaitForSingleObject(hnd) - așteptare și ocupare, ReleaseMutex(hnd) - eliberare, CloseHandle(hnd) - închidere. Poate fi folosit pentru a proteja împotriva repornirii programelor. 3) semafor -semafor. Obiectul kernel „semafor” este utilizat pentru contabilitatea resurselor și servește la limitarea accesului simultan la o resursă de mai multe fire. Folosind un semafor, puteți organiza munca programului în așa fel încât mai multe fire să poată accesa resursa în același timp, dar numărul acestor fire va fi limitat. La crearea unui semafor, este specificat numărul maxim de fire care pot funcționa simultan cu resursa. De fiecare dată când un program accesează un semafor, contorul de resurse al semaforului este decrementat cu unu. Când valoarea contorului de resurse devine zero, semaforul este indisponibil. creați CreateSemaphore, deschideți OpenSemaphore, luați WaitForSingleObject, lansați ReleaseSemaphore 4 ) eveniment -eveniment. Evenimentele, de obicei, notifică doar despre sfârșitul unei operațiuni, sunt, de asemenea, obiecte kernel. Nu numai că puteți elibera în mod explicit, dar există și o operație de setare a evenimentului. Evenimentele pot fi manuale (manuale) și simple (singure). Un singur eveniment este mai mult un steag general. Un eveniment este în starea semnalată dacă a fost setat de un fir de execuție. Dacă programul cere ca doar unul dintre fire să reacționeze la el în cazul unui eveniment, în timp ce toate celelalte fire de execuție continuă să aștepte, atunci este utilizat un singur eveniment. Un eveniment manual nu este doar un semnal comun în mai multe fire. Îndeplinește funcții ceva mai complexe. Orice thread poate seta acest eveniment sau îl poate reseta (șterge). Odată ce un eveniment este setat, acesta va rămâne în această stare pentru o perioadă de timp arbitrară, indiferent de câte fire de execuție așteaptă ca evenimentul să fie setat. Când toate firele care așteaptă acest eveniment primesc un mesaj că evenimentul a avut loc, acesta se va reseta automat. Funcții: SetEvent, ClearEvent, WaitForEvent. Tipuri de evenimente: 1) eveniment de resetare automată: WaitForSingleEvent. 2) un eveniment cu o resetare manuală (manual), atunci evenimentul trebuie resetat: ReleaseEvent. Unii teoreticieni evidențiază un alt obiect de sincronizare: WaitAbleTimer este un obiect nucleu al sistemului de operare care comută independent la o stare liberă după un interval de timp specificat (ceas alarmă).

Uneori, atunci când lucrați cu mai multe fire sau procese, devine necesar sincroniza execuția două sau mai multe dintre ele. Motivul pentru aceasta este cel mai adesea că două sau mai multe fire de execuție pot necesita acces la o resursă partajată care într-adevăr nu poate fi furnizat mai multor fire simultan. O resursă partajată este o resursă care poate fi accesată de mai multe sarcini care rulează în același timp.

Se apelează mecanismul care asigură procesul de sincronizare restricție de acces. Necesitatea acesteia apare și în cazurile în care un fir așteaptă un eveniment generat de un alt fir. Desigur, trebuie să existe o modalitate prin care primul thread va fi suspendat până la producerea evenimentului. După aceea, firul ar trebui să-și continue execuția.

Există două stări generale în care se poate afla o sarcină. În primul rând, sarcina poate să fie efectuate(sau fiți gata de executare de îndată ce are acces la resursele procesorului). În al doilea rând, sarcina poate fi blocat.În acest caz, execuția sa este suspendată până când resursa de care are nevoie este eliberată sau are loc un anumit eveniment.

Windows are servicii speciale care vă permit să restricționați accesul la resursele partajate într-un anumit mod, deoarece fără ajutorul sistemului de operare, un proces sau un fir separat nu poate determina singur dacă are acces unic la o resursă. Sistemul de operare Windows conține o procedură care, într-o singură operațiune continuă, verifică și, dacă este posibil, setează indicatorul de acces la resurse. În limbajul dezvoltatorilor de sisteme de operare, o astfel de operație este numită verificați și instalați funcționarea. Sunt apelate steagurile folosite pentru a asigura sincronizarea și controlul accesului la resurse semafoare(semafor). API-ul Win32 oferă suport pentru semafoare și alte obiecte de sincronizare. Biblioteca MFC include și suport pentru aceste obiecte.

Obiecte de sincronizare și clase mfc

Interfața Win32 acceptă patru tipuri de obiecte de sincronizare, toate bazate într-un fel sau altul pe conceptul de semafor.

Primul tip de obiect este semaforul însuși, sau semafor clasic (standard).. Permite unui număr limitat de procese și fire de execuție să acceseze o singură resursă. În acest caz, accesul la resursă este fie complet limitat (un singur fir sau proces poate accesa resursa într-o anumită perioadă de timp), fie doar un număr mic de fire și procese au acces simultan. Semaforele sunt implementate cu un contor care scade atunci când unei sarcini i se alocă un semafor și crește când o sarcină eliberează semaforul.

Al doilea tip de obiecte de sincronizare este semafor exclusiv (mutex).. Este conceput pentru a restricționa complet accesul la o resursă, astfel încât doar un proces sau fir de execuție să poată accesa resursa la un moment dat. De fapt, acesta este un tip special de semafor.

Al treilea tip de obiecte de sincronizare este eveniment, sau obiect eveniment. Este folosit pentru a bloca accesul la o resursă până când un alt proces sau fir de execuție declară că resursa poate fi utilizată. Astfel, acest obiect semnalează executarea evenimentului solicitat.

Folosind obiectul de sincronizare de al patrulea tip, este posibilă interzicerea executării anumitor secțiuni ale codului programului de către mai multe fire simultan. Pentru a face acest lucru, aceste parcele trebuie declarate ca secţiunea critică. Când un fir intră în această secțiune, altor fire le este interzis să facă același lucru până când primul fir iese din această secțiune.

Secțiunile critice, spre deosebire de alte tipuri de obiecte de sincronizare, sunt utilizate numai pentru sincronizarea firelor de execuție în cadrul unui singur proces. Alte tipuri de obiecte pot fi folosite pentru a sincroniza firele în cadrul unui proces sau pentru a sincroniza procese.

În MFC, mecanismul de sincronizare furnizat de interfața Win32 este suportat prin următoarele clase derivate din clasa CSyncObject:

    CCriticalSection- implementează o secțiune critică.

    CEvent- implementează obiectul eveniment

    CMutex- implementează un semafor exclusiv.

    Cemafor- implementează un semafor clasic.

Pe lângă aceste clase, MFC definește și două clase de sincronizare auxiliare: CSingleLockși CMultiLock. Acestea controlează accesul la obiectul de sincronizare și conțin metodele utilizate pentru a acorda și elibera astfel de obiecte. Clasă CSingleLock controlează accesul la un singur obiect de sincronizare și la clasă CMultiLock- la mai multe obiecte. În cele ce urmează, vom lua în considerare doar clasa CSingleLock.

Când orice obiect de sincronizare este creat, accesul la acesta poate fi controlat folosind clasa CSingleLock. Pentru a face acest lucru, trebuie mai întâi să creați un obiect de tip CSingleLock folosind constructorul:

CSingleLock(CSyncObject* pObject, BOOL bInitialLock = FALSE);

Primul parametru este un pointer către un obiect de sincronizare, cum ar fi un semafor. Valoarea celui de-al doilea parametru determină dacă constructorul ar trebui să încerce să acceseze obiectul dat. Dacă acest parametru este diferit de zero, atunci accesul va fi acordat, în caz contrar nu se va încerca accesul. Dacă accesul este acordat, atunci firul care a creat obiectul de clasă CSingleLock, va fi oprit până când obiectul de sincronizare corespunzător este eliberat prin metodă Deblocați clasă CSingleLock.

Odată ce un obiect de tip CSingleLock a fost creat, accesul la obiectul indicat de parametrul pObject poate fi controlat folosind două funcții: Lacătși Deblocați clasă CSingleLock.

Metodă Lacăt este conceput pentru a accesa obiectul la obiectul de sincronizare. Firul care l-a apelat este suspendat până la finalizarea metodei, adică până când resursa este accesată. Valoarea parametrului determină cât timp va aștepta funcția pentru a obține acces la obiectul necesar. De fiecare dată când metoda se finalizează cu succes, valoarea contorului asociat obiectului de sincronizare este decrementată cu unu.

Metodă Deblocați eliberează obiectul de sincronizare, permițând altor fire să folosească resursa. În prima variantă a metodei, valoarea contorului asociat obiectului dat este incrementată cu unu. În a doua opțiune, primul parametru determină cât de mult trebuie mărită această valoare. Al doilea parametru indică o variabilă în care va fi scrisă valoarea anterioară a contorului.

Când lucrezi cu o clasă CSingleLock Procedura generală pentru controlul accesului la o resursă este următoarea:

    creați un obiect de tip CSyncObj (de exemplu, un semafor) care va fi folosit pentru a controla accesul la resursă;

    folosind obiectul de sincronizare creat, creați un obiect de tip CSingleLock;

    apelați metoda Lock pentru a obține acces la resursă;

    efectuați un apel către resursă;

    apelați metoda Unlock pentru a elibera resursa.

Următoarele descrie cum să creați și să utilizați semafoare și obiecte eveniment. Odată ce înțelegeți aceste concepte, puteți învăța și utiliza cu ușurință celelalte două tipuri de obiecte de sincronizare: secțiuni critice și mutexuri.