Cunoștințe, prelegere, sincronizarea fluxurilor

Secțiuni critice

Ca parte a API-ului Windows, există funcții speciale și eficiente pentru organizarea intrării în și ieșirea din secțiunea critică a firelor unui proces în modul de utilizare. Acestea se numesc EnterCriticalSection și LeaveCriticalSection și au o structură preinicializată de tip CRITICAL_SECTION ca parametru.

O schemă de program aproximativă poate arăta astfel.

Funcțiile EnterCriticalSection și LeaveCriticalSection sunt implementate pe baza funcțiilor Interlocked, sunt executate într-o manieră atomică și funcționează foarte rapid. Este esențial ca, în eventualitatea imposibilității de a intra în secțiunea critică, fluxul trece într-o stare de așteptare. Ulterior, atunci când apare o astfel de oportunitate, fluxul va fi "trezit" și va fi capabil să încerce să intre în secțiunea critică. Mecanismul de trezire a firului este implementat folosind obiectul kernel "event", care este creat numai în cazul unei situații de conflict.

S-a spus deja că uneori, înainte de a bloca fluxul, este logic să o țineți pentru o vreme într-o stare de așteptare activă. Pentru funcția EnterCriticalSection efectuați un anumit număr de cicluri de spinlock. Se recomandă inițializarea secțiunii critice utilizând funcția InitalizeCriticalSectionAndSpinCount.

Rularea programului

Ca exercițiu independent se recomandă implementarea sincronizării în programul async de mai sus cu ajutorul primitivelor enumerate. Este important să nu uităm despre ieșirea corectă din secțiunea critică, adică, despre băieți folosind funcțiile EnterCriticalSection și LeaveCriticalSection.

Sincronizarea thread-urilor folosind obiecte kernel

Secțiunile critice discutate în secțiunea anterioară sunt potrivite pentru sincronizarea firelor unui singur proces. Sarcina sincronizării firelor de procese diferite este decisă prin utilizarea obiectelor kernel. Un obiect de kernel poate fi atribuit unui nume, acestea vă permit să specificați un timeout pentru intervalul de timp și să aveți o serie de alte posibilități pentru implementarea scenariilor flexibile de sincronizare. Cu toate acestea, utilizarea acestora este asociată cu trecerea la modul kernel (aproximativ 1000 de cicluri de procesor), adică funcționează oarecum mai lent decât secțiunile critice.

Aproape toate obiectele de kernel considerate mai devreme, incluzând procese, fire și fișiere, sunt potrivite pentru sarcinile de sincronizare. În contextul sarcinilor de sincronizare, se poate spune că fiecare obiect se află într-o stare liberă (semnal, stare semnalată) sau stare ocupată (stare nesigilată). Regulile pentru mutarea unui obiect de la o stare la alta depind de obiect. De exemplu, dacă un fir se execută, acesta este într-o stare de ocupat, iar dacă firul a finalizat cu succes semaforul de așteptare, atunci semaforul este într-o stare ocupat.

Firele sunt în stare de așteptare, în timp ce obiectele pe care le așteaptă sunt ocupate. Odată ce obiectul este eliberat, sistemul de operare trezește firul și permite continuarea execuției. Pentru a întrerupe fluxul și a pune-l în starea de așteptare a eliberării obiectului, utilizați funcția

unde hObject este descriptorul obiectului de kernel așteptat, iar al doilea parametru este timpul maxim de așteptare pentru obiect.

Firele creează un obiect kernel folosind familia de funcții Creare (CreateSemaphore, CreateThread, etc.), după care obiectul devine disponibil pentru toate firele procesului prin intermediul mânerului. O copie a mânerului poate fi obținută utilizând funcția DuplicateHandle și trecută la un alt proces, după care firele pot folosi acest obiect pentru sincronizare.

O altă modalitate mai obișnuită de a obține un mâner este să deschideți un obiect existent după nume, deoarece multe obiecte au nume în spațiul de nume al obiectului. Numele obiectului este unul dintre parametrii funcției Creare. Cunoscând numele obiectului. curs de apa. care are drepturile de acces necesare, își folosește descriptorul utilizând funcțiile Open. Reamintim că în structura care descrie obiectul. există un contor de referință pentru acesta, care este mărit cu 1 atunci când obiectul este deschis și scade cu 1 atunci când acesta este închis.

Să aruncăm o privire mai atentă asupra acelor obiecte de kernel care sunt destinate rezolvării problemelor de sincronizare.

Este cunoscut faptul că semafoarele Dijkstra propusă în 1965 a reprezentat-o ​​variabilă în spațiul nucleu, accesul la care, după inițializarea, aceasta poate fi efectuată prin două operații atomice. așteptați și semnalul (în Windows, această funcție WaitForSingleObject și, respectiv, ReleaseSemaphore).

Semaphorele sunt de obicei utilizate pentru contabilizarea resurselor (numărul curent de resurse este specificat de variabila S) și sunt create folosind funcția CreateSemaphore. numărul de parametri din care include valoarea inițială și cea maximă a variabilei. Valoarea actuală nu poate fi mai mare decât valoarea maximă și negativă. Valoarea lui S. este egală cu zero înseamnă că semaforul este ocupat.

Următorul exemplu este un exemplu de sincronizare a programului async cu semaphore.

Acest program sincronizează acțiunile a două fire. care oferă același rezultat pentru toate runurile de programe, se realizează utilizând două semafoare, aproximativ modul în care se face în sarcina producător-consumator. vezi de exemplu [Tanenbaum]. Curenții alternativ se deschid reciproc spre locul critic. Firul SecondThread începe să funcționeze mai întâi. deoarece valoarea contorului exploatației semaforului a fost inițializată de una în momentul creării acestui semafor. Se recomandă sincronizarea folosind semafoare de fire de diferite procese ca un exercițiu independent.

Mutexurile sunt, de asemenea, obiecte kernel utilizate pentru sincronizare, dar ele sunt mai simple decât semafoarele, deoarece ele controlează accesul la o singură resursă și, prin urmare, nu conțin contoare. În esență, ele se comportă ca niște secțiuni critice, dar pot sincroniza accesul firelor de procese diferite. Mutexul este inițializat cu funcția CreateMutex. Pentru a intra în secțiunea critică, utilizați funcția WaitForSingleObject. și pentru ieșire - ReleaseMutex.

Dacă firul se termină fără a elibera mutexul, firul intră într-o stare liberă. Diferența față de semafoare este aceea că firul care ocupă mutexul are dreptul să-l dețină. Numai acest thread poate elibera mutexul. Prin urmare, vizualizarea unui mutex ca semafor cu o valoare maximă de 1 nu este în întregime adevărată.

Obiectele "evenimente" sunt obiectele cele mai primitive ale kernel-ului. Acestea sunt concepute pentru a informa unul fir al celuilalt despre sfârșitul unei operații. Evenimentele sunt create de funcția CreateEvent. Cea mai simplă versiune de sincronizare: traducerea unui eveniment într-o stare ocupată utilizând funcția WaitForSingleObject și eliberarea acesteia cu funcția SetEvent.

În manualul de programare [Richter]. [Hart]. Luați în considerare scenarii mai complexe legate de tipul de eveniment (resetare manuală și resetare automată) și cu controlul sincronizării grupurilor de fire, precum și o serie de funcții utile suplimentare.

Dezvoltarea programelor în care mutexurile și evenimentele sunt folosite pentru a rezolva sarcinile de sincronizare este recomandată ca un exercițiu independent.

Informații sumare despre obiectele din kernel

Pentru instrucțiuni de programare, consultați, de exemplu, [Richter]. și MSDN conține, de asemenea, informații despre alte obiecte kernel în ceea ce privește sincronizarea firului.

În particular, următoarele proprietăți ale obiectelor există:

  • procesul și fluxul se află într-o stare de ocupare, atunci când sunt active și într-o stare liberă, atunci când acestea sunt finalizate;
  • Fișierul este în stare ocupată când se emite o cerere de intrare / ieșire și în starea inactiv când operația I / O este finalizată;
  • comanda de schimbare a fișierelor este în stare ocupată atunci când nu există schimbări în sistemul de fișiere și în cel liber, când sunt detectate modificările;
  • și așa mai departe.

Sincronizarea în miez

Soluția la problema excluziunii reciproce este deosebit de importantă pentru un sistem atât de complex ca nucleul Windows.

Una dintre problemele rezultă din faptul că codul de nucleu de multe ori funcționează pe ICCV prioritare (nivelurile ICCV discutate în „Concepte de bază ale sistemelor de operare Windows“), nivelurile „DPC / expediție“ sau „mai sus“, cunoscut sub numele de „ICCV de mare“. Aceasta înseamnă mijloace tradiționale de sincronizare. asociată cu fluxul de suspendare nu poate fi utilizat, deoarece procedura pentru a programa și începerea unui alt fir are o prioritate mai mică. Cu toate acestea, există pericolul de apariție a unor evenimente ale căror ICCV mai mare decât secțiunea critică ICCV, care în acest caz va fi preempted. Prin urmare, în astfel de situații recurg pentru a primi, care se numește „întrerupere interzisă“ [Karpov]. [Tanenbaum]. În cazul Windows acest lucru se realizează prin creșterea artificială a secțiunii critice ICCV până la cel mai înalt nivel utilizat în orice posibilă sursă de întrerupere. Ca urmare, secțiunea critică poate desfășura activitatea nestingherit.

Din păcate, pentru sistemele multiprocesor, o astfel de strategie nu este bună. Întreruperile întreruperilor pe unul dintre procesoare nu exclud întreruperile pe un alt procesor, care își poate continua activitatea și poate avea acces la date critice. În acest caz, avem nevoie de un protocol special pentru stabilirea excluderii reciproce. Baza acestui protocol este instalarea unei variabile de blocare (blocare-schimbare), asociată cu fiecare structură de date globală, utilizând comanda TSL. Deoarece blocarea este instalată ca urmare a așteptării active. atunci ei spun că nucleul codifică setările (capturează) spinlock-ul. Setarea de spinlock are loc la niveluri ridicate ale IRQL, astfel încât codul kernelului care capturează spinlock-ul și îl ține să execute secțiunea critică a codului nu este niciodată forțat să iasă. Instalarea și eliberarea blocărilor de spin se realizează prin funcțiile kernelului KeAcquireSpinlock și KeReleaseSpinlock. care sunt utilizate în mod activ în driverele de kernel și de dispozitive. La sistemele uniprocesor, instalarea și îndepărtarea blocurilor de centrifugare se realizează pur și simplu prin ridicarea și scăderea IRQL.

În cele din urmă, având un set de resurse globale, în acest caz - centrifugi, este necesar să rezolvăm problema apariției posibilelor impasuri [Sorokin]. De exemplu, firul 1 captează blocarea 1 și firul 2 captează blocarea 2. Apoi firul 1 încearcă să captureze blocarea 2 și blocarea blocurilor 2 a filetului 2. Ca urmare, ambele fire de kernel atârnă. Una dintre soluțiile la această problemă este numerotarea tuturor resurselor și alocarea lor numai în ordinea numărului în creștere [Karpov]. În cazul ferestrelor, există o ierarhie a încuietorilor de rotire: toate sunt plasate în listă în ordinea descrescătoare a frecvenței utilizării și trebuie să fie capturate în ordinea în care sunt enumerate.

În cazul IRQL scăzut, sincronizarea este efectuată într-un mod tradițional - folosind obiecte kernel.

concluzie

Problema non-determinismului este una din cheile în mediile paralele de calcul. Soluția tradițională este organizarea excluderii reciproce. Pentru a sincroniza cu utilizarea unei variabile de blocare, se folosesc funcții interblocate care susțin atomicitatea unei anumite secvențe de operații. Este mai ușor să organizați întreruperea firelor dintr-un proces utilizând primitivul secțiunii critice. Pentru scenarii mai complexe, se recomandă utilizarea obiectelor de kernel, în special semafoare, mutexuri și evenimente. Este considerată problema sincronizării în miez, soluția principală fiind instalarea și eliberarea încuietorilor de centrifugare.







Articole similare

Trimiteți-le prietenilor: