Obiecte de sincronizare în Windows. Instrumente de sincronizare a firelor de operare în sistemul de operare Windows (secțiuni critice, mutexuri, semafore, evenimente) Metoda de sincronizare în secțiunea critică api win32

Firele pot fi într-una din mai multe stări:

    Gata(gata) – situat în bazinul de fire în așteptare de execuție;

    Funcţionare(execuție) - rulează pe procesor;

    Aşteptare(în așteptare), numit și inactiv sau suspendat, suspendat - într-o stare de așteptare, care se termină cu firul care începe să se execute (starea Running) sau intră în stare Gata;

    Terminat (finalizare) - execuția tuturor comenzilor firului este încheiată. Poate fi șters ulterior.

Dacă fluxul nu este șters, sistemul îl poate reseta la starea inițială pentru utilizare ulterioară.

Sincronizarea firelor

Firele care rulează trebuie adesea să comunice într-un fel. De exemplu, dacă mai multe fire de execuție încearcă să acceseze anumite date globale, atunci fiecare fir de execuție trebuie să protejeze datele împotriva modificării de către un alt fir. Uneori, un thread trebuie să știe când un alt thread va finaliza o sarcină. O astfel de interacțiune este obligatorie între firele de execuție atât ale aceluiași proces, cât și ale diferitelor procese. Sincronizarea firelor ( fir sincronizare

) este un termen general care se referă la procesul de interacțiune și interconectare a firelor. Vă rugăm să rețineți că sincronizarea firelor de execuție necesită ca sistemul de operare însuși să acționeze ca intermediar. Firele nu pot interacționa între ele fără participarea ei. Există mai multe metode pentru sincronizarea firelor în Win32. Se întâmplă ca în situatii specifice

o metodă este mai de preferat decât cealaltă. Să aruncăm o privire rapidă asupra acestor metode.

Secțiuni critice

O secțiune critică este o secțiune de cod care poate fi executată doar de un fir la un moment dat. Dacă codul folosit pentru a inițializa o matrice este plasat într-o secțiune critică, atunci alte fire de execuție nu vor putea intra în acea secțiune de cod până când primul fir de execuție nu a terminat de executat-o.

Înainte de a utiliza o secțiune critică, trebuie să o inițializați utilizând procedura API Win32 InitializeCriticalSection(), care este definită (în Delphi) după cum urmează:

procedura InitializeCriticalSection(var IpCriticalSection: TRTLCriticalSection); stdcall;

Parametrul IpCriticalSection este o înregistrare de tip TRTLCriticalSection care este transmisă prin referință. Definiția exactă a intrării TRTLCriticalSection nu contează prea mult, deoarece este puțin probabil să aveți nevoie vreodată să vă uitați la conținutul acesteia. Tot ce trebuie să faceți este să treceți o intrare neinițializată la parametrul IpCtitical Section, iar această intrare va fi imediat populată de procedură.

După completarea intrării în program, puteți crea o secțiune critică plasând o parte din textul acesteia între apelurile la funcțiile EnterCriticalSection() și LeaveCriticalSection(). Aceste proceduri sunt definite după cum urmează:

procedura EnterCriticalSection(var IpCriticalSection: TRTLCriticalSection); stdcall;

procedura LeaveCriticalSection(var IpCriticalSection: TRTLCriticalSection); stdcall;

Parametrul IpCriticalSection care este transmis acestor proceduri nu este altceva decât o intrare creată de procedura InitializeCriticalSection().

Funcţie Introduceți secțiunea critică verifică dacă un alt fir execută deja secțiunea critică a programului său asociată cu obiectul secțiune critică dată. Dacă nu, firul primește permisiunea de a-și executa codul critic sau, mai degrabă, nu este împiedicat să facă acest lucru. Dacă da, atunci firul care face cererea este pus într-o stare de așteptare și se face o înregistrare a cererii. Deoarece înregistrările trebuie create, obiectul secțiune critică este o structură de date.

Când funcția LeaveCriticalSection apelat de un fir de execuție care are în prezent permisiunea de a-și executa secțiunea critică de cod asociată cu un anumit obiect de secțiune critică, sistemul poate verifica pentru a vedea dacă există un alt fir de execuție în coadă care așteaptă ca obiectul respectiv să fie eliberat. Apoi, sistemul poate elimina firul de așteptare din starea de așteptare și își va continua activitatea (în intervalele de timp alocate acestuia).

Când ați terminat de lucrat cu înregistrarea TRTLCriticalSection, trebuie să o eliberați apelând procedura DeleteCriticalSection(), care este definită după cum urmează:

procedura DeleteCriticalSection(var IpCriticalSection: TRTLCriticalSection); stdcall;

Dacă fluxul nu este șters, sistemul îl poate reseta la starea inițială pentru utilizare ulterioară.

Când construiți o aplicație cu mai multe fire de execuție, trebuie să vă asigurați că orice parte a datelor partajate este protejată de posibilitatea ca mai multe fire de execuție să-și modifice valorile. Având în vedere că toate firele dintr-un AppDomain au acces simultan la datele aplicației partajate, imaginați-vă ce s-ar putea întâmpla dacă mai multe fire de execuție accesează același element de date în același timp. Deoarece planificatorul de fire va suspenda aleator firele de execuție, ce se întâmplă dacă firul A este întrerupt înainte de a termina de rulat? Iată ce: firul B va citi apoi datele instabile.

Pentru a ilustra problema concurenței, să luăm în considerare următorul exemplu:

Clasa publică MyTheard ( public void ThreadNumbers() ( // Informații despre threadul Console.WriteLine("(0) firul folosește metoda ThreadNumbers",Thread.CurrentThread.Name); // Imprimă numerele Console.Write("Numbers : "); pentru (int i = 0; i

Înainte de a ne uita la testele, să clarificăm din nou problema. Un fir primar din acest domeniu de aplicație își începe existența prin generarea a zece fire de lucru secundare. Fiecare fir de lucru trebuie să apeleze metoda ThreadNumbers() pe aceeași instanță MyTheard. Având în vedere că nu au fost luate măsuri pentru a bloca resursele partajate ale acestui obiect (consola), există șanse mari ca firul de execuție curent să fie oprit înainte ca metoda ThreadNumbers() să poată imprima rezultatele complete. Deoarece nu se știe exact când s-ar putea întâmpla acest lucru (sau chiar dacă s-ar putea întâmpla deloc), vor apărea rezultate neașteptate. De exemplu, este posibil să vedeți următoarea ieșire:

Este clar că aici este o problemă. Pe măsură ce fiecare fir solicită MyTheard să imprime date numerice, programatorul de fire le schimbă în fundal. Rezultatul este o ieșire inconsistentă. Pentru a rezolva astfel de probleme în C# folosim sincronizare.

Sincronizarea se bazează pe conceptul de blocare, prin care se organizează controlul accesului la un bloc de cod dintr-un obiect. Când un obiect este blocat de un fir, alte fire nu pot accesa blocul de cod blocat. Când blocarea este eliberată de un fir, obiectul devine disponibil pentru utilizare de către un alt fir.

Facilitatea de blocare este construită în limbajul C#. Datorită acestui lucru, toate obiectele pot fi sincronizate. Sincronizarea este organizată folosind un cuvânt cheie blocare. A fost inclus în C# încă de la început și, prin urmare, este mult mai ușor de utilizat decât pare la prima vedere. De fapt, sincronizarea obiectelor are loc aproape fără probleme în multe programe C#.

Mai jos este forma generalaîncuietori:

lock(lockObj) ( // instrucțiuni sincronizate)

Unde blocareObj denotă o referire la obiectul sincronizat. Dacă trebuie să sincronizați doar o singură instrucțiune, atunci acoladele nu sunt necesare. Operatorul de blocare se asigură că o bucată de cod protejată de o blocare pe un anumit obiect va fi folosită numai de firul care dobândește blocarea respectivă. Și toate celelalte fire sunt blocate până când blocarea este eliberată. Blocarea este eliberată la finalizarea fragmentului de cod pe care îl protejează.

Un obiect care reprezintă o resursă sincronizată este considerat a fi blocat. În unele cazuri, se dovedește a fi o instanță a resursei în sine sau o instanță arbitrară a unui obiect utilizat pentru sincronizare. Cu toate acestea, rețineți că obiectul care este blocat nu ar trebui să fie accesibil public, deoarece altfel poate fi blocat dintr-o altă bucată de cod necontrolată din program și în viitor nu va fi deloc deblocat.

În trecut, construcția de blocare (acest) era folosită foarte frecvent pentru a bloca obiecte. Dar este util doar dacă aceasta este o referire la un obiect privat. Din cauza posibilelor erori de programare și conceptuale la care poate duce constructul de blocare, utilizarea sa nu mai este recomandată. În schimb, este mai bine să creați un obiect privat și apoi să îl blocați.

Să modificăm exemplul anterior adăugându-i sincronizare:

Clasa publică MyTheard ( obiect privat threadLock = obiect nou(); public void ThreadNumbers() ( // Folosește blocarea markerului de blocare (threadLock) ( // Informații despre firul de execuție Console.WriteLine ("(0) threadul folosește metoda ThreadNumbers" , Thread CurrentThread.Name // Imprimă numere Console.Write("Numere: ");

Odată ce un fir de execuție intră în contextul de blocare, simbolul de blocare (în acest caz, obiectul curent) va deveni indisponibil pentru alte fire până când blocarea este eliberată la ieșirea din contextul de blocare. Astfel, dacă firul A dobândește un token de blocare, alte fire de execuție nu vor putea intra în niciunul dintre contexte folosind același simbol până când firul A îl eliberează.

Buna ziua! Astăzi vom continua să luăm în considerare caracteristicile programării cu mai multe fire și să vorbim despre sincronizarea firelor.

Ce este „sincronizarea”? În afara domeniului de programare, aceasta se referă la un fel de configurare care permite două dispozitive sau programe să lucreze împreună. De exemplu, un smartphone și un computer pot fi sincronizate cu un cont Google, cont personal pe site - cu conturile în rețelele sociale pentru a vă conecta cu ei. Sincronizarea firelor de execuție are un înțeles similar: stabilește modul în care firele de execuție interacționează între ele. În prelegerile anterioare, firele noastre au trăit și au lucrat separat unele de altele. Unul număra ceva, al doilea dormea, al treilea afișa ceva pe consolă, dar nu interacționau unul cu celălalt. În programele reale, astfel de situații sunt rare. Mai multe fire pot funcționa activ, de exemplu, cu același set de date și pot schimba ceva în el. Acest lucru creează probleme. Imaginați-vă că mai multe fire de discuție scriu text în aceeași locație - de exemplu, un fișier text sau consola. Acest fișier sau consolă devine în acest caz o resursă partajată. Threadurile nu știu unul despre existența celuilalt, așa că pur și simplu notează tot ceea ce pot gestiona în timpul pe care planificatorul de fire le alocă. Într-o prelegere recentă a cursului, am avut un exemplu la ce ar duce acest lucru, să ne amintim: motivul constă în faptul că firele de discuție au funcționat cu o resursă partajată, consola, fără a coordona acțiunile între ele. Dacă planificatorul thread-ului a alocat timp pentru Thread-1, acesta scrie instantaneu totul în consolă. Ceea ce alte fire au reușit deja să scrie sau nu au avut timp să scrie nu este important. Rezultatul, după cum puteți vedea, este dezastruos. Prin urmare, în programarea multi-threaded a fost introdus un concept special mutex (din engleză „mutex”, „excludere reciprocă” - „excludere reciprocă”). Sarcina Mutex- asigurați un mecanism astfel încât doar un fir să aibă acces la un obiect la un anumit moment. Dacă Thread-1 a dobândit mutex-ul obiectului A, alte fire de execuție nu vor avea acces la acesta pentru a schimba ceva în el. Până când mutex-ul obiectului A este eliberat, firele rămase vor fi forțate să aștepte. Exemplu din viața reală: imaginează-ți că tu și alți 10 străini participați la un antrenament. Trebuie să vă exprimați pe rând idei și să discutați ceva. Dar, din moment ce vă vedeți pentru prima dată, pentru a nu vă întrerupe în mod constant și pentru a nu aluneca într-un tumult, folosiți regula „minge care vorbește”: doar o singură persoană poate vorbi - cea care are mingea în mâinile lui. Astfel discuția se dovedește a fi adecvată și fructuoasă. Deci, un mutex, în esență, este o astfel de minge. Dacă mutexul unui obiect este în mâinile unui fir, alte fire nu vor putea accesa obiectul. Nu trebuie să faceți nimic pentru a crea un mutex: este deja încorporat în clasa Object, ceea ce înseamnă că fiecare obiect din Java are unul.

Cum funcționează operatorul sincronizat

Să facem cunoștință cu un nou cuvânt cheie - sincronizate. Acesta marchează o anumită bucată din codul nostru. Dacă un bloc de cod este marcat cu cuvântul cheie sincronizat, înseamnă că blocul poate fi executat doar de un fir la un moment dat. Sincronizarea poate fi implementată în diferite moduri. De exemplu, creați o metodă întreagă sincronizată: public synchronized void doSomething() ( //...logica metodei) Sau scrieți un bloc de cod în care sincronizarea este efectuată pe un obiect: public class Main ( private Object obj = new Object () ; public void doSomething () ( synchronized (obj) ( ) ) ) Sensul este simplu. Dacă un fir de execuție intră într-un bloc de cod care este marcat cu cuvântul sincronizat, acesta dobândește instantaneu mutex-ul obiectului, iar toate celelalte fire care încearcă să intre în același bloc sau metodă sunt forțate să aștepte până când firul anterior își finalizează activitatea și eliberează monitor. Apropo! În prelegerile cursului ați văzut deja exemple de sincronizat, dar arătau diferit: public void swap () ( sincronizat (acest) ( //...logica metodei) ) Subiectul este nou pentru tine și, desigur, va exista confuzie cu sintaxa la început. Prin urmare, amintiți-vă imediat pentru a nu vă încurca mai târziu în metodele de scriere. Aceste două moduri de a scrie înseamnă același lucru: public void swap () ( sincronizat (acesta) ( //...logica metodei) ) public synchronized void swap () ( ) ) În primul caz, creați un bloc de cod sincronizat imediat după introducerea metodei. Este sincronizat de acest obiect, adică de obiectul curent. Și în al doilea exemplu ai pus cuvântul sincronizat pe întreaga metodă. Nu mai este nevoie de a indica în mod explicit vreun obiect pe care se realizează sincronizarea. Odată ce o metodă întreagă este marcată cu un cuvânt, această metodă va fi sincronizată automat pentru toate obiectele clasei. Să nu ne adâncim în discuții despre care metodă este mai bună. Deocamdată, alegeți ce vă place mai mult :) Principalul lucru este de reținut: puteți declara o metodă sincronizată doar atunci când toată logica din interiorul ei este executată de un fir în același timp. De exemplu, în acest caz, ar fi o eroare să faci metoda doSomething() sincronizată: public class Main ( private Object obj = new Object () ; public void doSomething () ( //... ceva logică disponibilă pentru toate firele sincronizat (obj) ( //logică care este disponibilă doar pentru un fir la un moment dat) ) ) După cum puteți vedea, o parte a metodei conține o logică pentru care nu este necesară sincronizarea. Codul din acesta poate fi executat de mai multe fire simultan și toate sunt critice locuri importante alocate unui bloc sincronizat separat. Și încă un lucru. Să ne uităm la microscop la exemplul nostru de schimb de nume din prelegere: public void swap () ( sincronizat (acest) ( //...logica metodei } } Vă rugăm să rețineți: sincronizarea se realizează folosind acest . Adică pentru un anumit obiect MyClass. Imaginați-vă că avem 2 fire (Thread-1 și Thread-2) și un singur obiect MyClass myClass . În acest caz, dacă Thread-1 apelează metoda myClass.swap(), mutex-ul obiectului va fi ocupat, iar Thread-2, când încearcă să apeleze myClass.swap(), se va bloca așteptând ca mutexul să devină liber. Dacă avem 2 fire și 2 obiecte MyClass - myClass1 și myClass2 - pe diferite obiecte firele noastre pot executa cu ușurință simultan metode sincronizate. Primul thread se execută: myClass1. swap(); Al doilea face: myClass2. swap();În acest caz cuvânt cheie.

sincronizat în cadrul metodei swap() nu va afecta funcționarea programului, deoarece sincronizarea este efectuată pe un anumit obiect. Și în acest din urmă caz, avem 2 obiecte Prin urmare, firele nu creează probleme unul altuia. La urma urmelor

doua obiecte au 2 mutexuri diferite si dobandirea lor este independenta una de alta Caracteristici de sincronizare în metode statice?

clasa MyClass ( private static String name1 = "Olya" ; private static String name2 = "Lena" ; public static sincronizat void swap () ( String s = name1; name1 = name2; name2 = s; ) ) Nu este clar ce va fi îndeplinește rolul mutex în acest caz. La urma urmei, am decis deja că fiecare obiect are un mutex. Dar problema este că pentru a apela metoda statică MyClass.swap() nu avem nevoie de obiecte: metoda este statică! Deci ce urmează? :/ De fapt, nu este nicio problemă cu asta. Creatorii Java s-au ocupat de tot :) Dacă metoda care conține logica critică „multithreaded” este statică, sincronizarea va fi efectuată pe clasă. Pentru o mai mare claritate, codul de mai sus poate fi rescris ca: clasa MyClass ( private static String name1 = "Olya" ; private static String name2 = "Lena" ; public static void swap () ( sincronizat (MyClass. class ) ( String s = name1 ; name1 = name2; name2 = s ) ) ) În principiu, te-ai fi putut gândi la asta: deoarece nu există obiecte, atunci mecanismul de sincronizare trebuie să fie cumva „conectat” în clase. Așa este: puteți, de asemenea, să vă sincronizați între clase.

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 de executat. Este important să înțelegeți că nu procesele rulează, ci mai degrabă firele.

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

În funcție de situație, firele pot fi în trei stări. În primul rând, un fir se poate executa atunci când i se alocă timp CPU, adică. poate fi în stare de activitate. În al doilea rând, poate fi inactiv și așteaptă ca procesorul să fie alocat, de exemplu. fi într-o stare de pregătire. Și există o a treia stare, de asemenea foarte importantă - starea de blocare. Când un fir este blocat, acesta nu este alocat deloc. De obicei, un bloc este plasat în așteptarea unui eveniment. Când are loc acest eveniment, firul de execuție este mutat automat din starea blocată în starea gata. De exemplu, dacă un fir efectuează calcule, iar celălalt trebuie să aștepte ca rezultatele să le salveze pe disc. Al doilea ar putea folosi o buclă de genul „while(!isCalcFinished) continue;”, dar este ușor de verificat în practică că în timpul execuției acestei bucle procesorul este 100% ocupat (aceasta se numește așteptare activă). Asemenea cicluri ar trebui evitate dacă este posibil, în care mecanismul de blocare oferă o asistență neprețuită. Al doilea thread se poate bloca până când primul thread declanșează un eveniment care indică faptul că citirea este completă.

Sincronizarea firelor în sistemul de operare Windows

Windows implementează multitasking preventiv - asta î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 transfera controlul și de aceea, dacă o aplicație îngheța, computerul trebuia repornit.

Toate firele de execuție aparținând aceluiași proces au anumite resurse comune - cum ar fi spațiul de adrese 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 o resursă partajată, iar sistemul trece la un alt fir de execuție folosind aceeași resursă, atunci rezultatul muncii acestor fire de execuție 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 de execuție folosesc orice resursă partajată, apare această problemă.

Exemplu. Fire nesincronizate: dacă întrerupeți temporar firul de afișare (pauză), firul de execuție al 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; }

Acesta este motivul pentru care este necesar 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 programatic, sunt partajate de toate firele din sistem (unele 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 disc, un port, o intrare de bază de date, un obiect GDI și chiar o variabilă globală de program (care poate fi accesată de firele care aparțin aceluiași proces).

Există mai multe obiecte de sincronizare, dintre care cele mai importante sunt mutexul, secțiunea critică, evenimentul și semaforul. 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 (atunci când un fir așteaptă ca un alt fir sau proces să se finalizeze); 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 de semnal. Pentru fiecare tip de obiect această stare are un sens diferit. Thread-urile pot verifica starea curentă a unui obiect și/sau aștepta o schimbare în această stare și, astfel, își pot coordona 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 cu obiecte de sincronizare sunt atomice (indivizibile.

Lucrul cu obiecte de sincronizare

Pentru a crea unul sau altul obiect de sincronizare, este numită o funcție WinAPI specială de tip Create... (de exemplu, CreateMutex). Acest apel returnează un handle unui obiect (HANDLE) care poate fi folosit de toate firele de execuție aparținând acestui proces. Este posibil să accesați un obiect de sincronizare dintr-un alt proces - fie prin moștenirea unui handle la acest obiect, fie, de preferință, folosind un apel la funcția de deschidere a obiectului (Open...). 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). De exemplu, nu puteți crea un eveniment și un semafor cu același nume.

Folosind descriptorul existent al unui obiect, puteți determina starea lui curentă. Acest lucru se face folosind așa-numitul. funcții în așteptare. Funcția cel mai frecvent utilizată este WaitForSingleObject. Această funcție ia doi parametri, primul fiind mânerul obiectului, al doilea este timeout-ul în ms. Funcția returnează WAIT_OBJECT_0 dacă obiectul este într-o stare semnalizată, WAIT_TIMEOUT dacă a expirat și WAIT_ABANDONED dacă obiectul mutex nu a fost eliberat înainte de a ieși din firul său proprietar. Dacă timeout-ul este specificat ca zero, funcția returnează rezultatul imediat, în caz contrar, așteaptă perioada specificată. Dacă starea obiectului devine semnal î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, funcția va aștepta la nesfârșit până când starea obiectului devine semnal.

Un fapt foarte important este că apelarea unei funcții de așteptare blochează firul curent, adică. În timp ce un fir de execuție se află în starea inactivă, nu i se alocă timp CPU.

o metodă este mai de preferat decât cealaltă. Să aruncăm o privire rapidă asupra acestor metode.

Un obiect de secțiune critică ajută programatorul să izoleze secțiunea de cod în care un fir accesează o resursă partajată și să prevină 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 fir apoi încearcă să intre în aceeași secțiune critică, execuția sa se va întrerupe până când primul fir de execuție părăsește secțiunea apelând LeaveCriticalSection. Folosit numai pentru firele unui proces. Ordinea de intrare în secțiunea critică nu este definită.

Există, de asemenea, o funcție TryEnterCriticalSection care verifică dacă secțiunea critică este ocupată în prezent. Cu ajutorul său, firul de execuție, în așteptarea accesului la o resursă, nu poate fi blocat, ci poate efectua 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; }

Excluderi reciproce

Obiectele de excludere reciprocă (mutex, mutex - de la MUTual EXclusion) vă permit să coordonați excluderea reciprocă a accesului la o resursă partajată. Starea semnalului unui obiect (adică starea „setată”) corespunde unui moment în timp în care obiectul nu aparține niciunui fir și poate fi „capturat”. În schimb, starea „resetare” (non-semnal) corespunde momentului în care un fir de execuție 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 un mutex, iar următoarele primesc un mâner pentru un obiect deja existent. Acest lucru permite mai multor fire de execuție să obțină un mâner pentru același mutex, eliberând programatorul de a-și face griji cu privire la cine creează de fapt mutexul. Dacă se folosește această abordare, este recomandabil să setați steag-ul bInitialOwner la FALSE, altfel vor exista unele dificultăți în determinarea creatorului real al mutex-ului.

Fire multiple pot obține un mâner pentru același mutex, permițând comunicarea între procese. Următoarele mecanisme pentru această abordare pot fi utilizate:

  • Un proces copil creat folosind funcția CreateProcess poate moșteni un handle mutex dacă parametrul lpMutexAttributes a fost specificat la crearea mutex-ului cu 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 o excepție reciprocă ca aparținând firului curent, trebuie să apelați una dintre funcțiile de așteptare. Firul care deține obiectul îl poate recâștiga de câte ori dorește (acest lucru nu va duce la autoblocare), dar trebuie să-l elibereze de același număr de ori folosind funcția ReleaseMutex.

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

Exemplu. Sincronizarea firelor 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; }

Evenimente

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, iar restul va continua să aștepte.

Funcția CreateEvent creează un obiect eveniment, SetEvent - setează evenimentul la starea semnal, ResetEvent - resetează evenimentul. Funcția PulseEvent setează un eveniment, iar după ce firele care așteaptă acest eveniment sunt reluate (toate cu resetare manuală și doar una cu resetare 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 își permite să fie „capturat” de un anumit număr de fire. După aceasta, „capturarea” va fi imposibilă până când unul dintre firele care a „capturat” anterior semaforul îl eliberează. Semaforele sunt folosite pentru a limita numărul de fire care lucrează simultan cu o resursă. Numărul maxim de fire este transferat obiectului în timpul inițializării după fiecare „captură” contorul semaforului este micșorat. Starea semnalului corespunde unei valori a contorului mai mare decât zero. Când contorul este zero, semaforul este considerat neinstalat (resetat).

Funcția CreateSemaphore creează un obiect semafor indicând valoarea sa inițială maximă posibilă, OpenSemaphore - returnează un descriptor al unui semafor existent, semaforul este capturat folosind funcții de așteptare, iar valoarea semaforului este micșorată cu una, ReleaseSemaphore - semaforul este eliberat cu semaforul valoare crescută 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 îl monitorizează singure - execuția lor este atomică. Aceste funcții sunt 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 convenabil de utilizat 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 inclus în mediile de programare Borland Delphi sau CBuilder, precum și MSDN, furnizat ca parte a sistemului de programare Visual C.

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ă simultan de mai multe sarcini care rulează.

Se apelează mecanismul care asigură procesul de sincronizare restrictionarea accesului. 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 fir de discuție va fi suspendat înainte ca evenimentul să aibă loc. După aceasta, 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 executat de îndată ce obține acces la resursele CPU). Î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, în timpul unei operații continue, verifică și, dacă este posibil, setează indicatorul de acces la resurse. În limbajul dezvoltatorilor de sisteme de operare, această operație este numită verificarea si operatia de instalare. Sunt apelate steaguri 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 se bazează cumva 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 primesc acces simultan. Semaforele sunt implementate folosind un contor a cărui valoare scade atunci când un semafor este alocat unei sarcini și crește atunci când sarcina eliberează un semafor.

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ă finalizarea evenimentului solicitat.

Folosind un obiect de sincronizare de al patrulea tip, puteți interzice execuția anumitor secțiuni de cod de program de mai multe fire în același timp. Pentru a face acest lucru, aceste zone 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 secțiune.

Secțiunile critice, spre deosebire de alte tipuri de obiecte de sincronizare, sunt folosite doar pentru a sincroniza firele în cadrul aceluiași 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 de următoarele clase, care sunt derivate din clasa CSyncObject:

    CCriticalSection- implementeaza sectiunea critica.

    CEvent- implementează un obiect 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 metode utilizate pentru a furniza ș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.

Odată ce 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 transmite un pointer către un obiect de sincronizare, de exemplu un semafor. Valoarea celui de-al doilea parametru determină dacă constructorul ar trebui să încerce să acceseze acest obiect. Dacă acest parametru este diferit de zero, atunci se va obține accesul, în caz contrar nu se vor face încercări de a obține acces. Dacă accesul este acordat, atunci firul care a creat obiectul clasei CSingleLock, va fi oprit până când obiectul de sincronizare corespunzător este eliberat de metodă Deblocați clasă CSingleLock.

Când este creat un obiect de tip CSingleLock, accesul la obiectul indicat de pObject poate fi controlat folosind două funcții: BlocareŞi Deblocați clasă CSingleLock.

Metodă Blocare este destinat să obțină acces la un obiect la un obiect de sincronizare. Firul care l-a apelat este suspendat până la finalizarea metodei, adică până la obținerea accesului la resursă. Valoarea parametrului determină cât timp va aștepta funcția pentru accesul la obiectul necesar. De fiecare dată când o metodă 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 versiune a metodei, valoarea contorului asociat cu acest obiect este mărită cu unu. În a doua opțiune, primul parametru determină cât de mult trebuie crescută această valoare. Al doilea parametru indică variabila în care va fi scrisă valoarea contorului precedent.

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

    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;

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

    accesează o resursă;

    Apelați metoda Unlock pentru a elibera resursa.

Următoarele descriu 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ță alte două tipuri de obiecte de sincronizare: secțiuni critice și mutexuri.

airsoft-unity.ru - Portal minier - Tipuri de afaceri. Instrucţiuni. Companii. Marketing. Impozite