360 likes | 449 Views
P ROGRAMAREA MULTIPROCESOARELOR (1). Exploatarea concurentei la multiprocesoare Thread-uri in Windows Sincronizarea proceselor si thread-urilor Excludere mutuala Utilizarea sistemului de intreruperi Instructiunea TestAndSet Primitiva Fetch&Add. S isteme de operare multiprocesor:
E N D
PROGRAMAREA MULTIPROCESOARELOR (1) • Exploatarea concurentei la multiprocesoare • Thread-uri in Windows • Sincronizarea proceselor si thread-urilor • Excludere mutuala • Utilizarea sistemului de intreruperi • Instructiunea TestAndSet • Primitiva Fetch&Add
Sisteme de operare multiprocesor: S.O. master-slave: supervizorul este executat întotdeauna pe acelaşi procesor. Dacă un procesor slave necesită un serviciu al supervizorului => generează o cerere de servire şi aşteaptă până ce programul curent de pe procesorul master este întrerupt, iar supervizorul este disponibilizat. Caracteristici: -se simplifică problema conflictului de tabelă; -sistemul este inflexibil; -hardware şi software simple; -cădere catastrofală când procesorul master se defectează. S.O. cu supervizor separat pe fiecare procesor: fiecare procesor îşi execută propriile servicii. Fiecare procesor (supervizor) dispune de propriul său set de fişiere, dispozitive de I/E, tabele private. S.O. cu supervizor mobil: funcţia de master se deplasează de la un procesor la altul. Caracteristici: -conflictele pentru cereri de servicii sunt rezolvate prin priorităţi care pot fi setate static sau dinamic; -conflicte de acces la tabele.
Exploatarea concurentei la multiprocesoare O cale de a desemna concurenţa este utilizarea instrucţiunilor FORK şi JOIN: -FORK generează un proces nou : -FORK A: iniţiază un alt proces la adresa A şi continuă procesul curent; -FORK A,J: analog, dar în plus incrementează un contor de la adresa J; -FORK A,J,N: analog cu FORK A, dar în plus setează un contor de la adresa J la valoarea N. -JOIN aşteaptă terminarea unui proces creat anterior: JOIN J, decrementează contorul de la adresa J cu 1; dacă rezultatul decrementării este zero, procesul de la adresa J+1 este iniţiat, altfel procesorul care execută JOIN este eliberat. Deci, toate procesele execută în final o instrucţiune JOIN, exceptând ultimul proces.
Exemplu: utilizarea acestor instrucţiuni pentru controlul a trei procese concurente.
Extensie FORK-JOIN : limbajul structurat pe blocuri, propus de Dijkstra. În acest caz, fiecare proces dintr-un set de n procese S1, S2, …, Sn poate fi executat concurent utilizând următoarea construcţie cu cobegin-coend(parbegin-parend): begin S0; cobegin S1; S2; …; Sncoend; Sn+1 end Graf de precedenţă:
Instrucţiunile concurente pot fi structurate arbitrar: begin S0; cobegin S1; begin S2; cobegin S3; S4; S5coend S6end; S7 coend; S8 end
Paralelismul în cadrul ciclurilor.Primitivele utilizate pentru implementarea simplă a instrucţiunii parfor (parallel for) : PREP: un contor de căi paralele PPC (Parallel Path Counter) este iniţializat cu 1 şi se utilizează o stivă pentru PPC-uri în cazul buclelor structurate; AND(L): este bifurcaţia cu două căi, prin care PPCPPC+1, procesul de la adresa L este iniţiat, iar procesul curent este continuat; ALSO(L): analog cu AND(L), dar fără incrementarea contorului PPC; JOIN: PPCPPC–1, dacă PPC = 0, atunci PPC este extras din stivă şi prelucrarea continuă cu următoarea instrucţiune, altfel procesorul executând JOIN este eliberat; IDLE: încheie o cale şi eliberează procesorul care execută instrucţiunea.
Exemplu: implementarea unei instrucţiuni parfor, utilizând aceste primitive (instrucţiunea S este executată pentru fiecare valoare a lui i şi schema este independentă de numărul de procesoare disponibile în sistem). parfor i1 until n do begin . . . S . . . end
Exemplu: înmulţirea matrice – vector, CA*B, unde A este o matrice n*n, iar B şi C sunt vectori coloană n*1, pentru un n foarte mare. Algoritmul utilizează instrucţiunea parfor pentru a genera p procese independente (cu p divide pe n, n/p=s). parfor i 1 until p do begin for j (i-1)s+1 until si do begin C[j] 0; for k 1 until n do C[j] C[j] + A[j,k]*B[k] end end
Thread-uri in Windows Thread-ul = unitate de baza de utilizare a UCP, avand o stare redusa. Starea redusa <= gruparea unui numar de thread-uri corelate intre ele in cadrul unui task, pentru a partaja diferite resurse de calcul (memoria, fisiere, etc.). Un thread : stiva proprie si stare hardware (registre si indicatori). Comutarea thread-urilor aceluiasi proces este mai rapida, implicand salvarea starii hardware si a stivei. In Windows un proces este o colectie de resurse sistem, ca spatiu de adrese de memorie, handle-uri de fisiere si dispozitive, atribute de securitate, obiecte de sincronizare si in plus, cel putin un thread de executie. Fiecare thread dintr-un proces are resursele sale private: stivele nucleu si utilizator, un set de registre, atribute de obiecte si un contor de program.
Crearea thread-urilor Un proces incepe cu executia unui singur thread, din care se pot crea noi thread-uri: HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );
unde: lpThreadAttributes = pointer la ostructura care contine un descriptor de securitate. Daca descriptorul de securitate este NULL, handle-ul thread-ului are toate privilegiile de acces care i se adreseaza si nu va fi mostenit de nici un subproces. dwStackSize = dimensiunea stivei in octeti. Daca este NULL atunci are dimensiunea stivei thread-ului initial al procesului. lpStartAddress = adresa functiei cu care incepe executia noului thread. Functia trebuie sa aibe un argument si sa returneze un cod de iesire la terminare. lpParameter = parametrul furnizat acestei functii. Daca functia necesita mai multi parametri, acestea se incorporeaza intr-o singura structura de date si se furnizeaza un pointer la aceasta structura. dwCreationFlags = specifica daca thread-ul trebuie sa inceapa executia imediat sau nu. Daca acest parametru este 0 atunci se incepe imediat executia thread-ului; daca are valoarea CREATE_SUSPEND, thread-ul este creat, imediat suspendat si nu se executa decat la apelul functiei ResumeThread cu handle-ul sau ca parametru. lpThreadId = furnizeaza identificatorul thread-ului la crearea sa. Programatorul creaza o locatie DWORD si furnizeaza un pointer la aceasta locatie. ID-ul thread-ului curent : DWORD GetCurrentThreadId(void);
Functia CreateThread=> handle valid la crearea cu succes a unui nou thread sau 0 la insucces. In caz de esec: apel GetLastError=> codul de eroare. Exemplu: HANDLE hThread1, hRedPen; DWORD ThreadID1, ErrorCode; void ThreadProc (HANDLE hDrawPen); hThread1 = CreateThread ( NULL, //fara atribute de securitate 0, //dimensiune stiva implicita (LPTHREAD_START_ROUTINE)ThreadProc, //start thread hRedPen, //argument pentru noul thread (LPDWORD)&ThreadID1 //ID-ul thread-ului ); if (hThread1==0) { ErrorCode = GetLastError(); //handle eroare }
Se poate crea un nou thread care sa se execute in spatiul de adrese al altui proces : HANDLE CreateRemoteThread( HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ); unde hProcess= handle-ul procesului in care este lansat noul thread. Functia => handle-ul noului thread in caz de succes, sau NULL pentru insucces (necesar ca handle-ul procesului sa detina privilegiul de accesPROCESS_CREATE_THREAD si sa cunoasca adresa functiei de start). Noul thread : acces la toate resursele procesului => pentru scrierea depanatoarelor si a analizoarelor.
Terminarea thread-urilor Un thread isi incheie executia proprie: void ExitThread( DWORD dwExitCode ); Executia unui thread se poate incheia de alte thread-uri: BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode ); Incheierea tuturor thread-urilor dintr-un proces -> prin terminarea procesului: void ExitProcess( UINT uExitCode ); BOOL TerminateProcess( HANDLE hProcess, UINT uExitCode );
Executia unui thread se poate suspenda / relua: DWORD SuspendThread( HANDLE hThread ); DWORD ResumeThread( HANDLE hThread ); => valoarea precedenta a contorului de suspendari in caz de succes, sau valoarea –1 la insucces. Astfel: -un thread in executie: contor=0; -apel SuspendThread: contor++ (thread-ul este suspendat); -la fiecare apel ulterior SuspendThread: contor++ pana la o valoare maxima MAXIMUM_SUSPEND_COUNT; -fiecare apel ResumeThread: contor--; -daca contor == 0atunci thread-ul poate intra in executie.
Sincronizarea proceselor si thread-urilor Exemplu: procesele P1 si P2 acceseaza o variabila partajata reprezentand un cont bancar pentru actualizari ale sumei de bani:
Excludere mutuala Accesul la o variabila partajata pentru actualizare ~ sectiune critica (o secventa de instructiuni care este executata complet numai de un singur proces, inainte ca un alt proces sa acceseze acea sectiune critica). O problemă importantă: două sau mai multe procese concurente partajează date care sunt modificabile => segmente de program -> secţiuni critice. Supoziţii privind secţiunile critice: Excludere mutuală: cel mult un singur proces se poate afla într-o secţiune critică la un moment dat. Terminare: secţiunea critică este executată într-un timp finit. Planificare echilibrată: un proces aşteptând intrarea în secţiunea critică o va facce într-un timp finit.
Accesul mutual exclusiv la un set de variabile partajate se poate realiza prin diferite construcţii, printre care: MUTEXBEGIN şi MUTEXEND. Notaţia pentru a declara un set de variabile ca fiind partajate de un tip T: var v : shared T; O secţiune critică controlată de variabila partajată v : csect v do S; asociază o instrucţiune S cu o variabilă partajată v=>S trebuie să aibă acces exclusiv la v.
Secţiunile critice care se referă la variabile diferite se pot executa în paralel: var v : shared V; var w : shared W; cobegin csect v do P; csect w do Q coend Secţiunile critice pot fi structurate pe mai multe niveluri: csect v do begin . . . . . . . . csect w do S; . . . . . . . . end
Pericolul blocării: unul sau mai multe procese aşteaptă pentru evenimente care nu se produc niciodată. Exemplu: două procese concurente P1 şi P2 intră concomitent în secţiuni critice exterioare controlate de variabilele v şi w: cobegin P1: csect v docsect w do S1; P2: csect w docsect v do S2 coend
Accesul unui singur proces la o sectiune critica, in timp ce toate celelalte procese nu pot intra in sectiunea respectiva -> mecanismul de excludere mutuala. Etape: Solutii pentru implementarea excluderii mutuale: -de nivel scazut (prin hardware): instructiuni de invalidare / validare a intreruperilor (DI / EI), instructiuni de tip TestAndSet, etc; -de nivel ridicat (mecanisme / obiecte de sincronizare): mutex, semafoare, bariere, monitoare, etc.
Utilizarea sistemului de intreruperi Invalidarea / validarea intreruperilor -> solutie specifica sistemelor uniprocesor. di :invalidarea intreruperilor sectiune critica ei ;validarea intreruperilor => Solutie pesimista, ineficienta (poate bloca procese care nu au legatura cu sectiunea critica). Mecanismul periculos in programele utilizator insuficient testate! di :invalidarea intreruperilor halt ;oprire procesor si blocare sistem Solutia: functii sistem care utilizeaza di / ei apelate de programele utilizator. Dezavantaj: overhead la comutarea intre modul de executie utilizator si sistem.
Evenimente Când un proces aşteaptă pentru un eveniment execuţia următoarelor sale operaţii se amână până cand un alt proces semnalează producerea evenimentului. Exemplu:doua procese ciclice (operaţiile concurente wait şi signal accesează aceeaşi variabilă partajată e de tip eveniment): var e : shared event; cobegin cycle “sender” begin . . . signal(e) ; . . . end; cycle “receiver” begin . . . wait(e) ; . . . end coend
Instructiunea TestAndSet O implementare a accesului mutual exclusiv la o sectiune critica: operaţiileLOCK şi UNLOCK (se presupune v=0/1 dacă accesul este deschis/închis la secţiunea critică). Un proces testează valoarea variabilei v şi dacă găseşte starea deschis, atunci modifică starea la închis, într-o singură operaţie indivizibilă! Accesul la secţiunea critică se exprimă prin secvenţa: LOCK(v) execută secţiunea critică UNLOCK(v)
Operaţia LOCK : var x: sharedinteger; LOCK(x): begin var y: integer; y x; while y=1 do y x; //aşteaptă până deschis// x 1 //ocupă// end Implementarea operaţiei UNLOCK este următoarea: UNLOCK(x): x 0;
Mecanismul LOCK nesatisfăcător: două sau mai multe procese pot găsi x=0 şi intra simultan în secţiunea critică, înainte de a face x1. Solutia: instrucţiuni speciale!Exemplu:TEST_AND_SET(x), care testează şi setează o variabilă partajată într-un singur ciclu de memorie de tipul “read-modify-write”: var x: sharedinteger; TEST_AND_SET(x): begin var y: integer; y x; if y=0 then x 1; //indivizibil// end Operaţia LOCK poate fi rescrisă astfel: var x: sharedinteger; LOCK(x): begin var y: integer; repeat{ y TEST_AND_SET( x)} until y=0 end
Dezavantaj al operatieiLOCKcuTAS: pierderea de timp pentru procesele care intră în secţiunile critice (procesul care execută LOCK blocat până la permisiunea de intrare în secţiunea critică). LOCK şi UNLOCK nu în modul utilizator!=> cerere de supervizor la intrarea într-o secţiune critică => degradare a performanţelor sistemului. TAS şi în modul utilizator => când un proces în bucla de aşteptare din TAS=> creşte foarte mult rata cererilor de memorie. Soluţia: întârzierea cererilor la memorie cu T: var x: sharedinteger; LOCK(x): begin var y: integer; y TEST_AND_SET( x); while y 0 do begin PAUSE(T); y TEST_AND_SET( x) end end
Instrucţiune de forţare a excluderii mutuale pentru accesul la o variabilă partajată în locaţia de memorie m_addr:CAS (“comapare and swap”), cu doi operanzi suplimentari r_old şi r_new(care sunt registre ale procesorului). Definiţia instrucţiunii CAS r_old, r_new, m_addr : var m_addr: sharedaddress; var r_old, r_new: register; var z: CASflag; CAS: if r_old = m_addr then m_addr r_new; z 1 else r_old m_addr; z 0 Indicatorul z este setat dacă comparaţia furnizează egalitate. Execuţia instrucţiunii CAS (de fapt a instrucţiunii if) este indivizibilă.
Exemplu: utilizarea instrucţiunii CAS într-o coadă simplu înlănţuită partajată, accesată curent de două procese P1 şi P2. Operaţii: ENQUEUE(X): adaugă nodul X în coada listei specificată de pointerul TAIL; DEQUEUE(X): întoarce un pointer către capul şters al listei (HEAD). Variabilele HEAD şi TAIL sunt variabile partajate. Presupunând coada nevidă, pentru un sistem secvenţial monoprocesor: procedure ENQUEUE(X); var P: pointer; //P este local fiecărui apel al procedurii// begin LINK(X) ; //termină legătura ultimului nod// P TAIL; TAIL X; LINK(P) X //ataşează noul nod la coadă// end
Utilizarea procedurii într-un sistem paralel în care două procese concurente P1 şi P2 încearcă adăugarea câte unui nod în coadă : P1 începe execuţia procedurii pentru ataşarea nodului X şi după ce a executat PTAIL, se generează o întrerupere, când P2 execută procedura şi ataşează nodul Y. În continuare, se reia execuţia procedurii de către P1, care ataşează nodul X, iar nodul Y rămâne detaşat!
=> utilizarea instrucţiunii CAS pentru a actualiza P să indice ultimul nod ataşat (CAS asigură că starea logică a programului întrerupt, variabila P, este menţinută la reluarea programului, altfel P este setat la valoarea cea mai recentă a lui TAIL: procedure ENQUEUE(X); var P: pointer; //P este local fiecărui apel al procedurii// begin LINK(X) ; //termină legătura ultimului nod// P TAIL; repeat CAS P, X, TAIL until TAIL = X; LINK(P) X //ataşează noul nod la coadă// end
Primitiva Fetch&Add Primitivă care permite o anumită formă de concurenţă a accesului la o locaţie de memorie: “fetch-and-add”, F&A(X,e), unde X este o variabilă partajată întreagă, iar e este o expresie întreagă. Primitiva întoarce vechea valoare a lui X (Y) şi înlocuieşte conţinutul locaţiei de memorie prin suma Y+x, într-o singură operaţie indivizibilă! Mai multe operaţii iniţiate simultan de diferite procesoare => efectul final este acelaşi indiferent de ordine, dar valorile intermediare ale lui X specifică poziţia în cadrul ordinii. Exemplu: două procese P1 şi P2, care execută fiecare: P1: S1 F&A(X,e1); P2: S2 F&A(X,e2); => S1 şi S2 vor conţine valorile Y şi Y+e1 sau Y+e2 şi Y => X=Y+e1+e2. F&A implementat în comutatorul procesor-memorie.
Se presupune că există două cereri simultane, P1 având prioritate mai mare decât P2.