Afacerea mea este francize. Evaluări. Povesti de succes. Idei. Munca și educație
Cautare site

Care obiect de sincronizare are un contor de resurse. Instrumente de sincronizare a firelor în sistemul de operare Windows (secțiuni critice, mutexuri, semafore, evenimente)

Când mai multe procese (sau mai multe fire ale unui proces) accesează o resursă simultan, apare o problemă de sincronizare. Deoarece un fir de execuție în Win32 poate fi oprit în orice moment necunoscut de acesta în prealabil, este posibilă o situație când unul dintre fire de execuție nu a avut timp să finalizeze modificarea unei resurse (de exemplu, o zonă de memorie mapată la un fișier). ), dar a fost oprit și un alt thread a încercat să acceseze aceeași resursă. În acest moment, resursa se află într-o stare inconsistentă, iar consecințele accesării acesteia pot fi cele mai neașteptate - de la coruperea datelor până la încălcarea protecției memoriei.

Ideea principală din spatele sincronizării firelor în Win32 este utilizarea obiectelor de sincronizare și a funcțiilor de așteptare. Obiectele pot fi în una din două stări - Semnalat sau Nesemnalizat. Funcțiile de așteptare blochează execuția firului de execuție atâta timp cât obiectul specificat este în starea Nesemnalizat. Astfel, un fir de execuție care are nevoie de acces exclusiv la o resursă trebuie să seteze un obiect de sincronizare în starea nesemnalizată și, la finalizare, să-l reseteze la starea semnalată. Alte fire trebuie să apeleze o funcție de așteptare înainte de a accesa această resursă, ceea ce le va permite să aștepte ca resursa să devină disponibilă.

Să ne uităm la ce obiecte și funcții de sincronizare ne oferă API-ul Win32.

Funcții de sincronizare

Funcțiile de sincronizare se împart în două categorii principale: funcții care așteaptă pe un singur obiect și funcții care așteaptă pe unul dintre mai multe obiecte.

Funcții care așteaptă un singur obiect

Cea mai simplă funcție de așteptare este funcția WaitForSingleObject:

Funcția WaitForSingleObject(hHandle: THandle; // identificatorul obiectului dwMilliseconds: DWORD // perioada de așteptare): DWORD; stdcall;

Funcția așteaptă dwMilliseconds milisecunde pentru ca obiectul hHandle să intre în starea semnalului. Dacă treceți INFINITE ca parametru dwMilliseconds, funcția va aștepta nelimitat. Dacă dwMilliseconds este 0, atunci funcția verifică starea obiectului și revine imediat.

Funcția returnează una dintre următoarele valori:

Următorul fragment de cod interzice accesul la Action1 până când obiectul ObjectHandle este într-o stare semnalizată (de exemplu, în acest fel puteți aștepta finalizarea procesului, pasând ObjectHandle-ul său ca identificator obținut de funcția CreateProcess):

Var Motiv: DWORD; Cod de eroare: DWORD; Action1.Enabled:= FALSE; încercați să repetați Application.ProcessMessages; Motiv:= WailForSingleObject(ObjectHandle, 10); dacă Reason = WAIT_FAILED, atunci începe ErrorCode:= GetLastError; raise Exception.CreateFmt(‘Așteptați obiectul eșuat cu eroare: %d’, ); Sfârşit; până la Rațiune<>WAIT_TIMEOUT; în sfârșit Actionl.Enabled:= TRUE; Sfârşit;

În cazul în care, simultan cu așteptarea unui obiect, este necesară introducerea unui alt obiect într-o stare de semnal, se poate folosi funcția SignalObjectAndWait:

Funcția SignalObjectAndWait(hObjectToSignal: THandle; // obiect care va fi transferat în // starea semnalului hObjectToWaitOn: THandle; // obiect, care funcția așteaptă dwMilliseconds: DWORD; // perioada de așteptare bAlertable: BOOL // specifică dacă funcția trebuie să returneze // controlul în cazul unei solicitări pentru // finalizarea unei operațiuni I/O): DWORD; stdcall;

Valorile returnate sunt aceleași cu funcția WaitForSingleObject.

Obiectul hObjectToSignal poate fi un semafor, un eveniment sau un mutex. Parametrul bAlertable determină dacă așteptarea unui obiect va fi întreruptă dacă sistemul de operare solicită firului de execuție să finalizeze o operație I/O asincronă sau un apel de procedură asincronă. Acest lucru va fi discutat mai detaliat mai jos.

Funcții care așteaptă mai multe obiecte

Uneori este necesar să se întârzie execuția unui fir până când unul sau toate obiectele dintr-un grup sunt declanșate. Pentru a rezolva această problemă, se folosesc următoarele funcții:

Tip TWOHandleArray = matrice de THandle; PWOHandleArray = ^TWOHandleArray; function WaitForMultipleObjects(nCount: DWORD; // Setează numărul de obiecte lpHandles: PWOHandleArray; // Adresa unui tablou de obiecte bWaitAll: BOOL; // Stabilește dacă să aștepte toate // obiectele sau orice dwMilliseconds: DWORD // Perioada de așteptare ): DWORD; stdcall;

Funcția returnează una dintre următoarele valori:

Număr variind de la

WAIT_OBJECT_0 până la WAIT_OBJECT_0 + nCount – 1

Dacă bWaitAll este TRUE, atunci acest număr înseamnă că toate obiectele au intrat în starea semnal. Dacă FALSE, atunci scăzând WAIT_OBJECT_0 din valoarea returnată, obținem indexul obiectului din tabloul lpHandles

Număr variind de la

WAIT_ABANDONED_0 până la WAIT_ABANDONED_0 + nCount – 1

Dacă bWaitAll este TRUE, aceasta înseamnă că toate obiectele au intrat în starea semnalată, dar cel puțin unul dintre firele de execuție care le-au deținut a ieșit fără ca obiectul să fie semnalat. Dacă FALSE, atunci scădeți din valoarea returnată. valorile WAIT_ABANDONED_0, vom primi indexul obiectului din tabloul lpHandles, în timp ce firul, proprietar al acestei proprietăți s-a încheiat fără a-i face un semnal
WAIT_TIMEOUT Perioada de așteptare a expirat
WAIT_FAILED a avut loc o eroare

De exemplu, în următorul fragment de cod, programul încearcă să modifice două resurse diferite partajate între fire:

Var Handle: matrice de THandle; Motiv: DWORD; RestIndex: Integer; ... Mânere := OpenMutex(SYNCHRONIZE, FALSE, 'FirstResource'); Mânere := OpenMutex(SYNCHRONIZE, FALSE, 'SecondResource'); // Așteptăm primul obiect Reason:= WaitForMultipleObjects(2, @Handles, FALSE, INFINITE); caz Motivul WAIT_FAILED: RaiseLastWin32Error; WAIT_OBJECT_0, WAIT_ABANDONED_0: începe ModifyFirstResource; RestIndex:= 1; Sfârşit; WAIT_OBJECT_0 + 1, WAIT_ABANDONED_0 + 1: începe ModifySecondResource; RestIndex:= 0; Sfârşit; // WAIT_TIMEOUT nu poate avea loc la sfârșit; // Acum așteptați ca următorul obiect să fie eliberat dacă WailForSingleObject(Handles, INFINITE) = WAIT_FAILED apoi RaiseLastWin32Error; // Așteptați, modificați resursa rămasă dacă RestIndex = 0, apoi ModifyFirstResource altfel ModifySecondResource;

Tehnica descrisă mai sus poate fi folosită dacă știți sigur că întârzierea în așteptarea obiectului va fi neglijabilă. În caz contrar, programul tău va fi înghețat și nici măcar nu își va putea redesena fereastra. Dacă perioada de întârziere poate fi semnificativă, atunci trebuie să oferiți programului posibilitatea de a răspunde la mesajele Windows. Soluția poate fi să folosiți funcții cu o perioadă limitată de așteptare (și să apelați din nou dacă este returnat WAIT_TIMEOUT) sau funcția MsgWaitForMultipleObjects:

Funcția MsgWaitForMultipleObjects(nCount: DWORD; // numărul de obiecte de sincronizare var pHandle; // adresa unei matrice de obiecte fWaitAll: BOOL; // Stabilește dacă să aștepte toate // obiectele sau orice dwMilliseconds, // Perioada de așteptare dwWakeMask: DWORD // Tip eveniment , întrerupând așteptarea): DWORD; stdcall;

Principala diferență dintre această funcție și cea anterioară este parametrul dwWakeMask, care este o combinație a steagurilor de biți QS_XXX și specifică tipurile de mesaje care întrerup așteptarea funcției, indiferent de starea obiectelor așteptate. De exemplu, masca QS_KEY vă permite să întrerupeți așteptarea atunci când mesajele WM_KEYUP, WM_KEYDOWN, WM_SYSKEYUP sau WM_SYSKEYDOWN apar în coadă, iar masca QS_PAINT vă permite să întrerupeți mesajele WM_PAINT. Pentru o listă completă a valorilor permise pentru dwWakeMask, consultați documentația Windows SDK. Când mesajele corespunzătoare măștii specificate apar în coada firului de execuție care a apelat funcția, funcția returnează valoarea WAIT_OBJECT_0 + nCount. Odată ce programul dvs. primește această valoare, o poate procesa și apela din nou funcția de așteptare. Să luăm în considerare un exemplu cu lansarea unei aplicații externe (este necesar ca programul care apelează să nu răspundă la intrarea utilizatorului în timp ce rulează, dar fereastra acesteia trebuie să fie redesenată în continuare):

Procedura TForm1.Button1Click(Expeditor: TObject); var PI: TProcessInformation; SI: TStartupInfo; Motiv: DWORD; Msg:TMsg; begin // Initializeaza structura TStartupInfo FillChar(SI, SizeOf(SI), 0); SI.cb:= SizeOf(SI); // Rulați programul extern Win32Check(CreateProcess(NIL, "COMMAND.COM", NIL, NIL, FALSE, 0, NIL, NIL, SI, PI)); //************************************************ ** // Încercați să înlocuiți codul de mai jos cu linia // WaitForSingleObject(PI.hProcess, INFINITE); // și vedeți cum va reacționa programul la // mutarea altor ferestre deasupra ferestrei sale //******************************** ******************** repetare // Așteptați finalizarea procesului sau a mesajului copil // redesenați WM_PAINT Motiv:= MsgWaitForMultipleObjects(1, PI.hProcess, FALSE, INFINITE, QS_PAINT) ; dacă Reason = WAIT_OBJECT_0 + 1, atunci începe // WM_PAINT a apărut în coada de mesaje - Windows // necesită actualizarea ferestrei programului. // Eliminați mesajul din coadă PeekMessage(Msg, 0, WM_PAINT, WM_PAINT, PM_REMOVE); // Și redesenați actualizarea ferestrei noastre; Sfârşit; // Repetați bucla până când procesul copil iese până când Reason = WAIT_OBJECT_0; // Eliminați mesajele care s-au acumulat acolo din coadă în timp ce PeekMessage(Msg, 0, 0, 0, PM_REMOVE) face; CloseHandle(PI.hProcess); CloseHandle(PI.hThread) sfârşit;

Dacă ferestrele Windows sunt create explicit (folosind funcția CreateWindow) sau implicit (folosind TForm, DDE, COM) în firul care apelează funcțiile de așteptare, firul trebuie să proceseze mesajele. Deoarece mesajele difuzate sunt trimise către toate ferestrele din sistem, un fir de execuție care nu procesează mesajele poate cauza un blocaj (sistemul așteaptă ca un fir să proceseze un mesaj, un fir pentru ca sistem sau alte fire să elibereze un obiect) și cauza Windows să se blocheze. Dacă programul dvs. are astfel de fragmente, trebuie să utilizați MsgWaitForMultipleObjects sau MsgWaitForMultipleObjectsEx și să permiteți întreruperea procesării mesajelor în așteptare. Algoritmul este similar cu exemplul de mai sus.

Întrerupeți așteptarea unei cereri pentru a finaliza o operațiune I/O sau APC

Windows acceptă apeluri de procedură asincrone. Când fiecare fir este creat, îi este asociată o coadă de apeluri de procedură asincronă (coadă APC). Sistemul de operare (sau aplicația utilizator - folosind funcția QueueUserAPC) poate plasa solicitări pentru a executa funcții în contextul unui fir dat. Aceste funcții nu pot fi executate imediat deoarece firul de execuție poate fi ocupat. Prin urmare, sistemul de operare le apelează atunci când un fir apelează una dintre următoarele funcții de așteptare:

Funcția SleepEx(dwMilliseconds: DWORD; // Perioada de așteptare bAlertable: BOOL // specifică dacă funcția trebuie să returneze // controlul în cazul unei solicitări pentru // un apel de procedură asincronă): DWORD; stdcall; function WaitForSingleObjectEx(hHandle: THandle; // Identificator obiect dwMilliseconds: DWORD; // Perioada de așteptare bAlertable: BOOL // stabilește dacă funcția trebuie să returneze // controlul în cazul unei solicitări pentru // un apel de procedură asincronă): DWORD; stdcall; function WaitForMultipleObjectsEx(nCount: DWORD; // numărul de obiecte lpHandles: PWOHandleArray; // adresa matricei de identificatori de obiect bWaitAll: BOOL; // Stabilește dacă să aștepte toate // obiectele sau orice dwMilliseconds: DWORD; // Perioada de așteptare bAler : BOOL // stabilește dacă funcția trebuie să returneze // controlul în cazul unei solicitări pentru // un apel de procedură asincronă): DWORD; stdcall; function SignalObjectAndWait(hObjectToSignal: THandle; // obiect care va fi pus în // starea semnalului hObjectToWaitOn: THandle; // obiect pe care funcția îl așteaptă dwMilliseconds: DWORD; // perioada de așteptare bAlertable: BOOL // specifică dacă funcția ar trebui return // control în cazul unei solicitări pentru // un apel de procedură asincronă): DWORD; stdcall; function MsgWaitForMultipleObjectsEx(nCount: DWORD; // numărul de obiecte de sincronizare var pHandle; // adresa unei matrice de obiecte fWaitAll: BOOL; // Stabilește dacă să aștepte toate // obiectele sau orice dwMilliseconds, // Perioada de așteptare dwWakeMask: DWORD // Tipul evenimentului , întreruperea așteptării dwFlags: DWORD // Steaguri suplimentare): DWORD; stdcall;

Dacă bAlertable este TRUE (sau dacă dwFlags în funcția MsgWaitForMultipleObjectsEx conține MWMO_ALERTABLE), atunci când în coada APC apare o solicitare pentru un apel de procedură asincronă, sistemul de operare face apeluri la toate procedurile din coadă, după care funcția returnează valoarea WAIT_IO_COMPLETION.

Acest mecanism face posibilă implementarea, de exemplu, a I/O asincron. Un fir de execuție poate iniția execuția în fundal a uneia sau mai multor operațiuni I/O cu funcțiile ReadFileEx sau WriteFileEx, pasând acestora adresele funcțiilor de gestionare a finalizării operației. După finalizare, apelurile către aceste funcții vor fi puse în coadă la un apel de procedură asincronă. La rândul său, firul de execuție care a inițiat operațiunile, atunci când este gata să proceseze rezultatele, poate, folosind una dintre funcțiile de așteptare de mai sus, să permită sistemului de operare să apeleze funcțiile handler. Deoarece coada APC este implementată la nivelul nucleului sistemului de operare, este mai eficientă decât o coadă de mesaje și permite o I/O mult mai eficientă.

Eveniment

Evenimentul vă permite să notificați unul sau mai multe fire în așteptare că a avut loc un eveniment. Evenimentul are loc:

Pentru a crea un obiect, utilizați funcția CreateEvent:

Funcția CreateEvent(lpEventAttributes: PSecurityAttributes; // Adresa structurii // TSecurityAttributes bManualReset, // Stabilește dacă evenimentul va fi comutat // manual (TRUE) sau automat (FALSE) bInitialState: BOOL; // Setează starea inițială. Dacă TRUE - // obiect în stare de semnal lpName: PChar // Nume sau NIL dacă numele nu este necesar): THandle; stdcall; // Returnează identificatorul // obiectului creat Structura TSecurityAttributes este descrisă ca: TSecurityAttributes = înregistrarea nLength: DWORD; // Mărimea structurii ar trebui // să fie inițializată ca // SizeOf(TSecurityAttributes) lpSecurityDescriptor: Pointer; // Adresa descriptorului de securitate. Ignorat pe // Windows 95 și 98 // De obicei, puteți specifica NIL bInheritHandle: BOOL; // Stabilește dacă procesele copil // pot moșteni sfârșitul obiectului;

Dacă nu trebuie să setați drepturi de acces speciale în Windows NT sau posibilitatea ca procesele copil să moștenească obiectul, puteți trece NIL ca parametru lpEventAttributes. În acest caz, obiectul nu poate fi moștenit de procesele copil și primește un descriptor de securitate „implicit”.

Parametrul lpName vă permite să partajați obiecte între procese. Dacă lpName se potrivește cu numele unui obiect existent de tip Event, creat de procesul curent sau de orice alt proces, atunci funcția nu creează un obiect nou, ci returnează identificatorul unuia existent. Acest lucru ignoră parametrii bManualReset, bInitialState și lpSecurityDescriptor. Puteți verifica dacă un obiect a fost creat sau dacă unul existent este utilizat după cum urmează:

HEvent:= CreateEvent(NIL, TRUE, FALSE, 'EventName'); dacă hEvent = 0 atunci RaiseLastWin32Error; if GetLastError = ERROR_ALREADY_EXISTS then begin // Folosește obiectul creat anterior end;

Dacă un obiect este utilizat pentru sincronizare în cadrul unui singur proces, acesta poate fi declarat ca o variabilă globală și creat fără nume.

Numele obiectului nu trebuie să fie același cu numele oricărui obiect Semaphore, Mutex, Job, Waitable Timer sau FileMapping existent. Dacă numele se potrivesc, funcția returnează o eroare.

Dacă știți că un eveniment a fost deja creat, puteți utiliza funcția OpenEvent în loc de CreateEvent pentru a-l accesa:

Funcția OpenEvent(dwDesiredAccess: DWORD; // Setează drepturile de acces la obiectul bInheritHandle: BOOL; // Stabilește dacă obiectul poate fi moștenit // de către procesele copil lpName: PChar // Nume obiect): THandle; stdcall;

Funcția returnează identificatorul obiectului sau 0 în caz de eroare. Parametrul dwDesiredAccess poate fi una dintre următoarele valori:

După ce primiți identificatorul, puteți începe să îl utilizați. Următoarele funcții sunt disponibile pentru aceasta:

Funcția SetEvent(hEvent: THandle): BOOL; stdcall;

Setează un obiect într-o stare de alarmă

Funcția ResetEvent(hEvent: THandle): BOOL; stdcall;

Resetează un obiect, punându-l într-o stare nesemnalizată

Funcția PulseEvent(hEvent: THandle): BOOL; stdcall

Setează un obiect în starea de semnal, lasă toate funcțiile de așteptare care așteaptă pe acel obiect să ruleze și apoi îl resetează din nou.

API-ul Windows utilizează evenimente pentru a efectua operațiuni I/O asincrone. Următorul exemplu arată cum o aplicație inițiază scrierea în două fișiere simultan și apoi așteaptă ca scrierea să se finalizeze înainte de a continua; Această abordare poate oferi performanțe mai bune la intensitate I/O mare decât scrierile secvențiale:

Evenimente Var: matrice de THandle; // Matrice de obiecte de sincronizare Overlapped: matrice de TOoverlapped; ... // Creați obiecte de sincronizare Evenimente := CreateEvent(NIL, TRUE, FALSE, NIL); Evenimente := CreateEvent(NIL, TRUE, FALSE, NIL); // Inițializați structurile TOverlapped FillChar(Overlapped, SizeOf(Overlapped), 0); Overlapped.hEvent:= Evenimente; Overlapped.hEvent:= Evenimente; // Începeți să scrieți asincron în fișierele WriteFile(hFirstFile, FirstBuffer, SizeOf(FirstBuffer), FirstFileWritten, @Overlapped); WriteFile(hSecondFile, SecondBuffer, SizeOf(SecondBuffer), SecondFileWritten, @Overlapped); // Așteptați finalizarea scrierii în ambele fișiere WaitForMultipleObjects(2, @Events, TRUE, INFINITE); // Distruge obiectele de sincronizare CloseHandle(Events); CloseHandle(Evenimente)

Odată ce lucrul cu obiectul este finalizat, acesta trebuie distrus de funcția CloseHandle.

Delphi oferă o clasă TEvent care încapsulează funcționalitatea unui obiect Event. Clasa se află în modulul SyncObjs.pas și este declarată după cum urmează:

Tastați TWaitResult = (wrSignaled, wrTimeout, wrAbandoned, wrError); TEvent = class(THandleObject) constructor public Create(EventAttributes: PSecurityAttributes; ManualReset, InitialState: Boolean; const Name: string); funcția WaitFor(Timeout: DWORD): TWaitResult; procedura SetEvent; procedura ResetEvent; Sfârşit;

Scopul metodelor este evident din numele lor. Utilizarea acestei clase vă permite să nu intrați în complexitatea implementării funcțiilor numite Windows API. Pentru cele mai simple cazuri, se declară o altă clasă cu un constructor simplificat:

Tip TSimpleEvent = class(TEvent) constructor public Creare; Sfârşit; ... constructor TSimpleEvent.Create; începe FHandle:= CreateEvent(nil, True, False, nil); Sfârşit;

Mutex (exclusiv reciproc)

Un mutex este un obiect de sincronizare care este semnalat numai atunci când nu este deținut de niciun proces. Odată ce cel puțin un proces solicită dreptul de proprietate asupra unui mutex, acesta intră într-o stare nesemnalizată și rămâne astfel până când este eliberat de proprietar. Acest comportament vă permite să utilizați mutexuri pentru a sincroniza accesul partajat al mai multor procese la o resursă partajată. Pentru a crea un mutex, utilizați funcția:

Funcția CreateMutex(lpMutexAttributes: PSecurityAttributes; // Adresa structurii // TSecurityAttributes bInitialOwner: BOOL; // Stabilește dacă procesul va deține // mutex-ul imediat după creare lpName: PChar // Nume Mutex): THandle; stdcall;

Funcția returnează identificatorul obiectului creat sau 0. Dacă un mutex cu numele dat a fost deja creat, se returnează identificatorul acestuia. În acest caz, funcția GetLastError va returna codul de eroare ERROR_ALREDY_EXISTS. Numele nu trebuie să fie același cu numele unui obiect existent Semafor, Eveniment, Job, Temporizator așteptat sau FileMapping.

Dacă nu se știe dacă există deja un mutex cu același nume, programul nu trebuie să solicite proprietatea asupra obiectului la creare (adică trebuie să treacă bInitialOwner ca FALSE).

Dacă un mutex există deja, aplicația își poate obține identificatorul folosind funcția OpenMutex:

Funcția OpenMutex(dwDesiredAccess: DWORD; // Setează drepturile de acces la obiectul bInheritHandle: BOOL; // Stabilește dacă obiectul poate fi moștenit // de către procesele copil lpName: PChar // Nume obiect): THandle; stdcall;

Funcția returnează identificatorul mutexului deschis sau 0 în cazul unei erori. Mutexul intră în starea de semnal după ce funcția de așteptare la care a fost transmis identificatorul său se declanșează. Pentru a reveni la starea nesemnalizată, utilizați funcția ReleaseMutex:

Funcția ReleaseMutex(hMutex: THandle): BOOL; stdcall;

Dacă mai multe procese comunică, de exemplu printr-un fișier mapat în memorie, atunci fiecare trebuie să conțină următorul cod pentru a asigura accesul corect la resursa partajată:

Var Mutex: THandle; // La inițializarea programului Mutex:= CreateMutex(NIL, FALSE, 'UniqueMutexName'); dacă Mutex = 0 atunci RaiseLastWin32Error; ... // Acces la resursa WaitForSingleObject(Mutex, INFINITE); încercați // Accesarea resursei, capturarea mutexului asigură // că alte procese care încearcă să obțină acces // vor fi oprite la funcția WaitForSingleObject ... în sfârșit // Lucrul cu resursa este terminat, eliberați-l // pentru alte procese ReleaseMutex (Mutex) ; Sfârşit; ... // Când programul se termină CloseHandle(Mutex);

Este convenabil să încapsulați un astfel de cod într-o clasă care creează o resursă protejată. Un mutex are proprietăți și metode de operare pe o resursă, protejându-le folosind funcții de sincronizare.

Desigur, dacă lucrul cu o resursă poate dura o perioadă semnificativă de timp, ar trebui fie să utilizați funcția MsgWaitForSingleObject, fie să apelați WaitForSingleObject într-o buclă cu zero timeout, verificând codul de returnare. În caz contrar, cererea dvs. va fi înghețată. Protejați întotdeauna achiziția și eliberarea unui obiect de sincronizare cu o încercare ... în cele din urmă blocați, altfel o eroare în timpul lucrului cu resursa va bloca toate procesele care așteaptă să fie eliberată.

Semafor (semafor)

Un semafor este un numărător care conține un număr întreg care variază de la 0 la valoarea maximă specificată atunci când a fost creat. Contorul este decrementat de fiecare dată când un fir de execuție completează cu succes o funcție de așteptare care utilizează un semafor și este incrementat prin apelarea funcției ReleaseSemaphore. Când semaforul atinge valoarea 0, intră într-o stare de nesemnalizare; pentru orice alte valori de contor, starea lui este semnalizată. Acest comportament permite semaforului să fie folosit ca restrictor de acces la o resursă care acceptă un număr predefinit de conexiuni.

Pentru a crea un semafor, utilizați funcția CreateSemaphore:

Funcția CreateSemaphore(lpSemaphoreAttributes: PSecurityAttributes; // Adresă de structură // TSecurityAttributes lInitialCount, // Valoare inițială contor lMaximumCount: Longint; // Valoare maximă contor lpName: PChar // Nume obiect): THandle; stdcall;

Funcția returnează identificatorul semaforului creat sau 0 dacă obiectul nu a putut fi creat.

Parametrul lMaximumCount specifică valoarea maximă a contorului semaforului, lInitialCount specifică valoarea inițială a contorului și trebuie să fie în intervalul de la 0 la lMaximumCount. lpName specifică numele semaforului. Dacă sistemul are deja un semafor cu același nume, atunci nu se creează unul nou, dar se returnează identificatorul semaforului existent. Dacă semaforul este utilizat într-un singur proces, îl puteți crea fără nume trecând NIL ca lpName. Numele semaforului nu trebuie să fie același cu numele unui eveniment existent, mutex, temporizator așteptabil, job sau obiect de mapare a fișierelor.

ID-ul unui semafor creat anterior poate fi obținut și prin funcția OpenSemaphore:

Funcția OpenSemaphore(dwDesiredAccess: DWORD; // Setează drepturile de acces la obiectul bInheritHandle: BOOL; // Stabilește dacă obiectul poate fi moștenit // de către procesele copil lpName: PChar // Nume obiect): THandle; stdcall;

Parametrul dwDesiredAccess poate fi una dintre următoarele valori:

Pentru a crește contorul de semafor, utilizați funcția ReleaseSemaphore:

Funcția ReleaseSemaphore(hSemaphore: THandle; // Identificator semafor lReleaseCount: Longint; // Contorul va fi incrementat cu această valoare lpPreviousCount: Pointer // Adresa unei variabile de 32 de biți care // ia valoarea anterioară // a contorului) : BOOL; stdcall;

Dacă valoarea contorului după executarea funcției depășește valoarea maximă specificată pentru aceasta de funcția CreateSemaphore, atunci ReleaseSemaphore returnează FALSE și valoarea semaforului nu este modificată. Putem trece NIL ca parametru lpPreviousCount dacă nu avem nevoie de această valoare.

Să luăm în considerare un exemplu de aplicație care rulează mai multe sarcini în fire separate (de exemplu, un program pentru descărcarea în fundal a fișierelor de pe Internet). Dacă numărul de sarcini care rulează simultan este prea mare, acest lucru va duce la încărcare inutilă pe canal. Prin urmare, implementăm firele în care sarcina va fi executată în așa fel încât atunci când numărul lor depășește o valoare predeterminată, firul de execuție s-ar opri și așteaptă finalizarea sarcinilor executate anterior:

Unit Limitat Thread; interfața folosește clase; tip TLimitedThread = class(TThread) procedura Execute; trece peste; Sfârşit; implementarea folosește Windows; const MAX_THREAD_COUNT = 10; var Semafor: THandle; procedura TLimitedThread.Execute; începe // Decrementează contorul de semafor. Dacă până în acest moment rulează deja MAX_THREAD_COUNT fire de execuție, contorul este 0 și semaforul este // într-o stare nesemnalizată. Firul va fi înghețat până când unul dintre // care rulează mai devreme se termină. WaitForSingleObject(Semafor, INFINIT); // Iată codul responsabil pentru funcționalitatea firului de execuție, // de exemplu încărcarea unui fișier... // Firul de execuție și-a încheiat activitatea, crește contorul de semafor și permite // altor fire să înceapă procesarea. ReleaseSemaphore (Semafor, 1, NIL); Sfârşit; initialization // Creați un semafor când programul pornește Semaphore:= CreateSemaphore(NIL, MAX_THREAD_COUNT, MAX_THREAD_COUNT, NIL); finalizare // Distruge semaforul la finalizarea programului CloseHandle(Semaphore); Sfârşit;

Curs nr. 9. Sincronizarea proceselor și a firelor

1. Obiective și mijloace de sincronizare.

2. Mecanisme de sincronizare.

1.Obiective și mijloace de sincronizare

Există o clasă destul de largă de instrumente ale sistemului de operare care asigură sincronizarea reciprocă a proceselor și firelor. Necesitatea sincronizării firelor apare doar în multiprogramare sistem de operareși este asociat cu utilizarea în comun a hardware-ului și resurse informaționale sistem de calcul. Sincronizarea este necesară pentru a evita cursele și blocajele la schimbul de date între fire, partajarea datelor și accesarea procesorului și a dispozitivelor I/O.

În multe sisteme de operare, aceste instrumente sunt numite instrumente de comunicare interproces (IPC), ceea ce reflectă primatul istoric al conceptului de „proces” în raport cu conceptul de „fir”. De obicei, instrumentele IPC includ nu numai instrumente de sincronizare între procese, ci și instrumente de schimb de date între procese.

Execuția unui fir într-un mediu de multiprogramare este întotdeauna asincronă. Este foarte dificil să spunem cu deplină certitudine în ce stadiu de execuție va fi un proces la un anumit moment în timp. Chiar și în modul cu un singur program, nu este întotdeauna posibil să se estimeze cu exactitate timpul necesar pentru a finaliza o sarcină. Acest timp în multe cazuri depinde în mod semnificativ de valoarea datelor sursă, care afectează numărul de cicluri, direcția de ramificare a programului, timpul de execuție a operațiunilor I/O, etc. Deoarece datele sursă în momente diferite când sarcina este lansat poate fi diferit, la fel și timpul de execuție etapele individuale și sarcina în ansamblu este o valoare foarte incertă.


Și mai incert este timpul de execuție al unui program într-un sistem multiprogramare. Momentele în care firele sunt întrerupte, timpul petrecut în cozi pentru resursele partajate, ordinea în care firele sunt selectate pentru execuție - toate aceste evenimente sunt rezultatul unei confluențe a mai multor circumstanțe și pot fi interpretate ca aleatorii. În cel mai bun caz, se pot estima caracteristicile probabilistice ale unui proces de calcul, de exemplu, probabilitatea de finalizare a acestuia într-o anumită perioadă de timp.

Astfel, firele în cazul general (când programatorul nu a luat măsuri speciale pentru a le sincroniza) curg independent, asincron între ele. Acest lucru este valabil atât pentru firele din același proces care execută cod de program comun, cât și pentru firele din procese diferite, fiecare executând propriul program.

Orice interacțiune între procese sau fire este legată de acestea sincronizare, care constă în coordonarea vitezelor acestora prin suspendarea fluxului până la apariţia unui anumit eveniment şi apoi activarea acestuia la producerea acestui eveniment. Sincronizarea se află în centrul oricărei interacțiuni fir, fie că implică partajarea resurselor sau schimbul de date. De exemplu, firul de execuție care primește ar trebui să acceseze datele numai după ce acestea au fost stocate în tampon de către firul de execuție. Dacă firul de execuție a accesat datele înainte de a intra în buffer, atunci acesta trebuie suspendat.

Când partajați resurse hardware, sincronizarea este, de asemenea, absolut necesară. Când, de exemplu, un fir activ are nevoie de acces la un port serial și un alt fir în modul exclusiv lucrează cu acest port. acest momentîn starea de așteptare, sistemul de operare suspendă firul activ și nu îl activează până când portul de care are nevoie este liber. Sincronizarea cu evenimente externe sistemului informatic, de exemplu, reacția la apăsarea combinației de taste Ctrl+C, este adesea necesară.

În fiecare secundă, în sistem apar sute de evenimente legate de alocarea și eliberarea resurselor, iar sistemul de operare trebuie să aibă mijloace fiabile și eficiente care să-i permită să sincronizeze firele de execuție cu evenimentele care au loc în sistem.

Pentru a sincroniza firele programe de aplicație programatorul le poate folosi pe ambele fonduri proprii atât tehnicile de sincronizare, cât și instrumentele sistemului de operare. De exemplu, două fire ale aceluiași proces de aplicație își pot coordona activitatea folosind o variabilă booleană globală disponibilă pentru ambele, care este setată la unul atunci când are loc un eveniment, de exemplu, un fir produce datele necesare pentru ca celălalt să continue să funcționeze. Cu toate acestea, în multe cazuri, facilitățile de sincronizare oferite de sistemul de operare sub formă de apeluri de sistem sunt mai eficiente, sau chiar singurele posibile. Astfel, firele care aparțin unor procese diferite nu pot interfera în niciun fel cu munca celuilalt. Fără medierea sistemului de operare, aceștia nu se pot suspenda reciproc sau nu se pot notifica reciproc despre apariția unui eveniment. Instrumentele de sincronizare sunt folosite de sistemul de operare nu numai pentru a sincroniza procesele aplicației, ci și pentru nevoile sale interne.

De obicei, dezvoltatorii de sisteme de operare oferă o gamă largă de instrumente de sincronizare la dispoziția programatorilor de aplicații și sisteme. Aceste instrumente pot forma o ierarhie, atunci când cele mai complexe sunt construite pe baza unor instrumente mai simple și pot fi, de asemenea, specializate funcțional, de exemplu, instrumente pentru sincronizarea firelor unui proces, instrumente pentru sincronizarea firelor diferite procese la schimbul de date etc. Deseori funcţionalitate Diferite apeluri de sistem de sincronizare se suprapun, astfel încât un programator poate folosi mai multe apeluri pentru a rezolva o problemă, în funcție de preferințele sale personale.


Nevoia de sincronizare și cursă

Neglijarea problemelor de sincronizare într-un sistem cu mai multe fire poate duce la rezolvarea incorectă a problemei sau chiar la blocarea sistemului. Luați în considerare, de exemplu (Fig. 4.16), sarcina de a menține o bază de date cu clienții unei anumite întreprinderi. Fiecărui client i se atribuie o înregistrare separată în baza de date, care conține, printre alte câmpuri, câmpurile Comanda și Plata. Programul care întreține baza de date este conceput ca un singur proces care are mai multe fire, inclusiv thread A, care introduce în baza de date informații despre comenzile primite de la clienți, și thread B, care înregistrează în baza de date informații despre plățile clienților pentru facturi. Ambele fire de execuție lucrează împreună pe un fișier comun de bază de date folosind aceiași algoritmi, care includ trei pași.

2. Introduceți o nouă valoare în câmpul Comanda (pentru fluxul A) sau Plată (pentru fluxul B).

3. Returnați înregistrarea modificată în fișierul bazei de date.

https://pandia.ru/text/78/239/images/image002_238.gif" width="505" height="374 src=">

Orez. 4.17. Influența vitezelor relative de curgere asupra rezultatului rezolvării problemei

Secțiunea critică

Un concept important în sincronizarea firelor este conceptul de „secțiune critică” a unui program. Secțiunea critică este o parte a unui program al cărui rezultat de execuție se poate schimba în mod imprevizibil dacă variabilele legate de acea parte a programului sunt modificate de alte fire în timp ce execuția acelei părți nu este încă finalizată. Secțiunea critică este întotdeauna definită în raport cu anumite date critice dacă este modificat într-un mod necoordonat, pot apărea reacții nedorite. În exemplul anterior, datele critice au fost înregistrările din fișierul bazei de date. Toate firele care lucrează cu date critice trebuie să aibă o secțiune critică definită. Rețineți că, în fire diferite, secțiunea critică constă în general din secvențe diferite de comenzi.

Pentru a elimina efectul curselor asupra datelor critice, este necesar să vă asigurați că în secțiunea critică asociată cu acele date se află un singur fir în orice moment. Nu contează dacă acest fir este într-o stare activă sau suspendată. Această tehnică se numește excludere mutuala. Sistemul de operare folosește diferite moduri de a implementa excluderea reciprocă. Unele metode sunt potrivite pentru excluderea reciprocă atunci când numai firele unui proces intră în secțiunea critică, în timp ce altele pot oferi excluderea reciprocă pentru firele diferitelor procese.

Cea mai simplă și, în același timp, cea mai ineficientă modalitate de a asigura excluderea reciprocă este ca sistemul de operare să permită firului de execuție să dezactiveze orice întreruperi în timp ce se află în secțiunea critică. Cu toate acestea, această metodă practic nu este utilizată, deoarece este periculos să ai încredere într-un fir utilizator pentru a controla sistemul - poate ocupa procesorul mult timp, iar dacă un fir se blochează într-o secțiune critică, întregul sistem se va prăbuși, deoarece nu vor fi permise niciodată întreruperi.

2. Mecanisme de sincronizare.

Blocarea variabilelor

Pentru a sincroniza firele unui proces, un programator de aplicații poate folosi global blocarea variabilelor. Programatorul lucrează cu aceste variabile, la care toate firele de execuție ale procesului au acces direct, fără a recurge la apeluri de sistem OS.

Ceea ce ar dezactiva întreruperile pe parcursul întregii operațiuni de verificare și instalare.

Implementarea excluderii reciproce în modul descris mai sus are un dezavantaj semnificativ: în timpul în care un thread se află în secțiunea critică, un alt thread care are nevoie de aceeași resursă, având acces la procesor, va interoga continuu variabila de blocare, irosind timpul procesorului. alocat acestuia, care ar putea fi folosit pentru a executa un alt fir. Pentru a elimina acest dezavantaj, multe sisteme de operare oferă apeluri de sistem speciale pentru lucrul cu secțiuni critice.

În fig. Figura 4.19 arată cum aceste funcții implementează excluderea reciprocă în sistemul de operare Windows NT. Înainte de a începe modificarea datelor critice, firul de execuție emite apelul de sistem EnterCriticalSection(). Acest apel efectuează mai întâi, ca în cazul precedent, o verificare a variabilei de blocare care reflectă starea resursei critice. Dacă apelul de sistem determină că resursa este ocupată (F(D) = 0), spre deosebire de cazul precedent, acesta nu efectuează un sondaj ciclic, ci pune firul de execuție în starea de așteptare (D) și notează că acest curent trebuie activat atunci când resursa corespunzătoare devine disponibilă. Firul care se folosește în prezent această resursă, după ieșirea din secțiunea critică, trebuie să execute funcția de sistem LeaveCriticalSectionO, în urma căreia variabila de blocare ia valoarea corespunzătoare stării libere a resursei (F(D) = 1), iar sistemul de operare se uită prin coadă. de fire care așteaptă această resursă și transferă primul fir din coadă în starea de pregătire.

Costurile generale" href="/text/category/nakladnie_rashodi/" rel="bookmark">Costurile generale ale sistemului de operare pentru implementarea funcției de intrare și ieșire din secțiunea critică pot depăși economiile obținute.

Semafoare

O generalizare a variabilelor de blocare sunt așa-numitele Semafoare Dijkstra.În loc de variabile binare, Dijkstra a propus utilizarea variabilelor care pot lua valori întregi nenegative. Astfel de variabile, folosite pentru sincronizarea proceselor de calcul, sunt numite semafore.

Pentru a lucra cu semafore, sunt introduse două primitive, notate tradițional P și V. Fie variabila S să reprezinte un semafor. Apoi acțiunile V(S) și P(S) sunt definite după cum urmează.

* V(S): variabila S este mărită cu 1 ca o singură acțiune. Prelevarea, construirea și depozitarea nu pot fi întrerupte. Variabila S nu este accesată de alte fire în timp ce această operație este efectuată.

* P(S): Scade S cu 1 dacă este posibil. Dacă 5=0 și este imposibil să reduceți S rămânând în regiunea valorilor întregi nenegative, atunci operația de apelare a firului P așteaptă până când această reducere devine posibilă. O verificare și o scădere cu succes este, de asemenea, o operațiune indivizibilă.

Nu sunt permise întreruperi în timpul execuției primitivelor V și P.

În cazul special în care semaforul S poate lua doar valorile 0 și 1, acesta devine o variabilă de blocare, care din acest motiv este adesea numită semafor binar. Activitatea P are potențialul de a pune firul care o execută într-o stare de așteptare, în timp ce activitatea V poate, în anumite circumstanțe, să trezească un alt fir care a fost suspendat de activitatea P.

Să ne uităm la utilizarea semaforelor folosind un exemplu clasic de interacțiune a două fire care rulează în modul multiprogramare, dintre care unul scrie date în pool-ul de buffer, iar celălalt le citește din pool-ul de buffer. Lăsați pool-ul de buffer-uri să fie format din N tampoane, fiecare dintre acestea putând conține o intrare. În general, firul de scriere și firul de citire pot avea viteze diferite și pot accesa pool-ul de buffer cu intensitate diferită. Într-o perioadă, viteza de scriere poate depăși viteza de citire, în alta - invers. Pentru dreapta colaborare Firul de scriere trebuie să se întrerupă când toate bufferele sunt ocupate și să se trezească când cel puțin un buffer este eliberat. Spre deosebire de aceasta, thread-ul cititorului ar trebui să se întrerupă când toate bufferele sunt goale și să se trezească când apare cel puțin o scriere.

Să introducem două semafore: e - numărul de buffere goale, și f - numărul de buffer-uri umplute, iar în starea inițială e = N, a f = 0. Apoi funcționarea firelor cu un pool de buffer comun poate fi descrisă după cum urmează (Fig. 4.20).

Firul de scriere execută mai întâi o operație P(e), prin care verifică dacă există buffere goale în pool-ul de buffer. În conformitate cu semantica operației P, dacă semaforul e este egal cu 0 (adică nu există momente tampon libere), atunci firul de scriere intră în starea de așteptare. Dacă valoarea lui e este un număr pozitiv, atunci acesta reduce numărul de buffer-uri libere, scrie date în următorul buffer liber și apoi crește numărul de buffer-uri ocupate cu operația V(f). Firul de citire acționează într-un mod similar, cu diferența că începe prin verificarea bufferelor pline, iar după citirea datelor, crește numărul de buffere libere.

DIV_ADBLOCK860">

Un semafor poate fi folosit și ca variabilă de blocare. În exemplul discutat mai sus, pentru a elimina coliziunile atunci când lucrați cu o zonă de memorie partajată, vom presupune că scrierea și citirea din buffer sunt secțiuni critice. Vom asigura excluderea reciprocă folosind semaforul binar b (Fig. 4.21). Ambele fire, după verificarea disponibilității bufferelor, trebuie să verifice disponibilitatea secțiunii critice.

https://pandia.ru/text/78/239/images/image007_110.jpg" width="495" height="639 src=">

Orez. 4.22. Apariția blocajelor în timpul execuției programului

NOTĂ

Blocajele trebuie să fie distinse de cozile simple, deși ambele apar atunci când resursele sunt partajate și arată similar ca aspect: un fir este suspendat și așteaptă ca o resursă să devină liberă. Cu toate acestea, o coadă este un fenomen normal, un semn inerent al utilizării mari a resurselor atunci când solicitările sosesc aleatoriu. O coadă apare atunci când o resursă nu este disponibilă în acest moment, dar va fi eliberată după ceva timp, permițând firului de execuție să continue. Un impas, după cum sugerează și numele, este o situație oarecum de nerezolvat. O condiție necesară pentru ca un blocaj să apară este ca un fir să aibă nevoie de mai multe resurse simultan.

În exemplele luate în considerare, blocajul era format din două fire, dar mai multe fire se pot bloca reciproc. În fig. Figura 2.23 prezintă o astfel de distribuție a resurselor Ri între mai multe fire de execuție Tj, ceea ce a dus la apariția blocajelor. Săgețile indică cerințele de resurse ale fluxului. O săgeată solidă înseamnă că resursa corespunzătoare a fost alocată firului de execuție, iar o săgeată punctată conectează firul de execuție la resursa necesară, dar nu poate fi încă alocată deoarece este ocupată de un alt fir. De exemplu, firul T1 are nevoie de resursele R1 și R2 pentru a efectua lucrări, dintre care doar unul este alocat - R1, iar resursa R2 este deținută de firul T2. Niciunul dintre cele patru fire prezentate în figură nu își poate continua munca, deoarece nu dispun de toate resursele necesare pentru aceasta.

Incapacitatea thread-urilor de a finaliza munca pe care au început-o din cauza blocajelor reduce performanța sistemului de calcul. Prin urmare, se acordă multă atenție problemei prevenirii blocajelor. În cazul în care apare un blocaj, sistemul trebuie să ofere operatorului operator un mijloc prin care să poată recunoaște un blocaj și să-l distingă de un blocaj normal din cauza indisponibilității temporare a resurselor. În cele din urmă, dacă este diagnosticat un blocaj, atunci sunt necesare mijloace pentru a elimina blocajele și a restabili procesul normal de calcul.

Proprietarul" href="/text/category/vladeletc/" rel="bookmark">proprietarul, setându-l într-o stare nesemnalizată și intră în secțiunea critică. După ce firul a terminat lucrul cu datele critice, „oferă up” mutex-ul, punându-l în starea semnalată. În acest moment, mutex-ul este liber și nu aparține niciunui thread. Dacă vreun thread așteaptă să fie eliberat, atunci devine următorul proprietar al acestui mutex, la în același timp mutexul intră în starea nesemnalizată.

Obiect eveniment (în acest caz, cuvântul „eveniment” este folosit într-un sens restrâns, ca denumire tip specific obiecte de sincronizare) este utilizat de obicei nu pentru a accesa date, ci pentru a notifica alte fire de execuție că o anumită acțiune a fost finalizată. Să fie, de exemplu, într-o anumită aplicație, munca să fie organizată în așa fel încât un fir de execuție citește datele dintr-un fișier într-un buffer de memorie, iar alte fire de execuție procesează aceste date, apoi primul fir de execuție citește o nouă porțiune de date și alte fire de execuție. procesează-l din nou și așa mai departe. La începutul execuției, primul fir setează obiectul eveniment la o stare nesemnalizată. Toate celelalte fire de execuție au făcut un apel către Wait(X), unde X este un indicator de eveniment și sunt într-o stare suspendată, așteaptă ca evenimentul respectiv să apară. De îndată ce buffer-ul este plin, primul thread raportează acest lucru sistemului de operare apelând Set(X). Sistemul de operare scanează coada de fire în așteptare și activează orice fire care așteaptă acest eveniment.

Semnale

Semnal Permite unei sarcini să răspundă la un eveniment, a cărui sursă poate fi sistemul de operare sau o altă sarcină. Semnalele includ întreruperea unei sarcini și executarea acțiunilor predeterminate. Semnalele pot fi generate sincron, adică ca rezultat al lucrului procesului în sine, sau pot fi trimise unui proces printr-un alt proces, adică generate asincron. Semnalele sincrone provin cel mai adesea din sistemul de întrerupere al procesorului și indică acțiuni de proces care sunt blocate de hardware, cum ar fi împărțirea la zero, eroarea de adresare, încălcarea protecției memoriei etc.

Un exemplu de semnal asincron este un semnal de la un terminal. Multe sisteme de operare asigură eliminarea rapidă a unui proces din execuție. Pentru a face acest lucru, utilizatorul poate apăsa o anumită combinație de taste (Ctrl+C, Ctrl+Break), în urma căreia sistemul de operare generează un semnal și îl trimite procesului activ. Semnalul poate ajunge oricând în timpul execuției unui proces (adică este asincron), necesitând ca procesul să se termine imediat. În acest caz, răspunsul la semnal este finalizarea necondiționată a procesului.

Un set de semnale poate fi definit în sistem. Codul de program al procesului care a primit semnalul poate fie să-l ignore, fie să răspundă la el printr-o acțiune standard (de exemplu, ieșire), fie să efectueze acțiuni specifice definite de programatorul aplicației. În acest din urmă caz, este necesar să se furnizeze apeluri speciale de sistem în codul programului, cu ajutorul cărora sistemul de operare este informat care procedură trebuie efectuată ca răspuns la primirea unui anumit semnal.

Semnalele asigură comunicarea logică între procese și între procese și utilizatori (terminale). Deoarece trimiterea unui semnal necesită cunoașterea identificatorului de proces, interacțiunea prin semnale este posibilă numai între procesele înrudite care pot obține informații despre identificatorii celuilalt.

ÎN sisteme distribuite, constând din mai multe procesoare, fiecare dintre ele având propria sa RAM, variabile de blocare, semafore, semnale și alte facilități similare bazate pe memorie partajată sunt nepotrivite. În astfel de sisteme, sincronizarea poate fi realizată numai prin schimbul de mesaje.

Acest obiect de sincronizare poate fi utilizat numai local în cadrul procesului care l-a creat. Obiectele rămase pot fi folosite pentru a sincroniza fire de execuție ale 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ă anumite operațiuni, 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, ar putea fi convenabil să protejați funcțiile writer cu o astfel de secțiune, deoarece accesul simultan al mai multor writer ar trebui împiedicat.

Pentru secțiunea critică sunt introduse două operații:

intra in sectiune; Atâta timp cât orice fir se află în secțiunea critică, toate celelalte fire care încearcă să intre în el se vor opri automat și vor aștepta. Un fir care a intrat deja în această secțiune poate intra în ea 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, numărul de ori câte ori intră firul î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 acea secțiune.

În general, în alte API-uri non-Win32 (de exemplu, 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ă doar de un fir de aplicație. Adică, intrarea într-o secțiune critică este considerată ca o oprire temporară a mecanismului de comutare a firelor până la ieșirea din această secțiune. În API-ul Win32, secțiunile critice sunt tratate ca obiecte, ceea ce duce la o oarecare confuzie - sunt foarte asemănătoare în proprietăți cu obiectele exclusive fără nume ( mutex, vezi mai jos).

Când utilizați secțiuni critice, trebuie să vă asigurați că fragmente prea mari de cod nu sunt alocate secțiunii, deoarece acest lucru poate duce la întârzieri semnificative în execuția altor fire.

De exemplu, în legătură cu heap-urile deja discutate, nu are sens să protejăm toate funcțiile pentru lucrul cu heap-ul cu o secțiune critică, deoarece funcțiile de citire pot fi executate în paralel. Mai mult, folosirea unei secțiuni critice chiar și pentru sincronizarea scriitorilor pare de fapt 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 printr-o 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 au drepturi de acces aproximativ egale (de exemplu, este imposibil să distingem scriitorii puri și cititorii);

la construirea de obiecte de sincronizare compozite, formate din mai multe standard, pentru a proteja operațiile secvențiale pe un obiect compozit.

Sistemul de operare Windows acceptă patru tipuri de obiecte de sincronizare.

  • Primul tip este un semafor clasic și poate fi folosit pentru a controla accesul la anumite resurse printr-un număr limitat de fire. Resursa partajată în acest caz poate fi folosită de un singur thread, sau de un anumit număr de fire din setul de candidați pentru această resursă. Semaforele sunt implementate ca simple contoare care cresc atunci când un fir eliberează un semafor și scad când un fir ocupă un semafor.
  • Al doilea tip de obiect de sincronizare se numește semafor exclusiv (mutex). Semaforele de excludere sunt folosite pentru a partiționa resursele, astfel încât să poată fi folosite de un singur fir în orice moment. Evident, un semafor exclusiv este un tip special de semafor obișnuit.
  • Al treilea tip de obiecte de sincronizare este un eveniment. Evenimentele pot servi pentru a bloca accesul la o resursă până când un alt fir semnalează eliberarea acesteia.
  • Al patrulea tip de obiect de sincronizare este o secțiune critică. Odată ce un fir de execuție intră într-o secțiune critică, niciun alt fir de execuție nu poate începe să-l execute până când firul de execuție pe care îl rulează a părăsit-o.

Trebuie remarcat faptul că sincronizarea firelor de execuție folosind secțiuni critice poate fi efectuată numai în cadrul unui proces, deoarece este imposibil să transferați adresa secțiunii critice de la un proces la altul, deoarece secțiunea critică este situată în zona variabilelor globale a ​procesul de rulare.

Un semafor este creat folosind funcția CreateSemaphore. Numărul de sarcini care au acces simultan la o anumită resursă este determinat de unul dintre parametrii funcției. Dacă valoarea acestei funcții este 1, atunci semaforul acționează ca un semafor exclusiv. La creație de succes semaforul își întoarce mânerul, în caz contrar null. Funcția WaitForSingleObject oferă un mod de așteptare semafor. Unul dintre parametri specifică timpul de așteptare în milisecunde. Dacă valoarea acestui parametru este INFINIT, atunci timpul de expirare este nedefinit. La finalizarea cu succes a funcției, valoarea contorului asociat cu semaforul scade cu mai mult de unu. Funcția ReleaseSemaphore() eliberează un semafor, permițând unui alt fir să-l folosească.

Un semafor mutex exclusiv este creat folosind funcția CreateMutex(), care returnează identificatorul obiectului creat sau nul în caz de eroare. Dacă este necesar, obiectul este eliberat folosind funcția generică CloseHandle(). Cunoscând numele unui obiect mutex, îl puteți deschide folosind funcția OpenMutex(). Cu această funcție, mai multe fire pot deschide același obiect și apoi îl pot aștepta simultan. Odată ce numele unui obiect este cunoscut de un fir de execuție, acesta îl poate obține folosind funcțiile WaitForSingleObject sau WaitForMultipleObjects. Un obiect mutex este eliberat folosind funcția ReleaseMutex().

Un eveniment este creat folosind funcția CreateEvent. Returnează un handle pentru evenimentul generat sau null dacă nu reușește. După crearea unui eveniment, firul de execuție așteaptă pur și simplu apariția acestuia folosind funcția WaitForSingleObject, specificând mânerul acestui eveniment ca prim parametru. Astfel, execuția firului este suspendată până când apare evenimentul corespunzător. După ce funcția SetEvent este apelată, procesul care așteaptă acest eveniment folosind funcția WaitForSingleObject va continua să se execute. Evenimentul poate fi resetat folosind funcția ResetEvent.

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

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

    Alergare(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.

În Win32, există mai multe metode de sincronizare a firelor. Se întâmplă că într-o anumită situație o metodă este mai de preferat decât alta. Să aruncăm o privire rapidă asupra acestor metode.

Secțiuni critice

O metodă de sincronizare a firelor este utilizarea secțiunilor critice. Aceasta este singura metodă de sincronizare a firelor care nu necesită nucleul Windows. (Secțiunea critică nu este un obiect kernel.) Cu toate acestea, această metodă poate fi utilizată numai pentru a sincroniza firele de execuție ale unui singur proces.

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;