Mutex ( în engleză mutex , de la excluderea reciprocă - „excluderea reciprocă”) este o primitivă de sincronizare care asigură excluderea reciprocă a execuției secțiunilor critice de cod [1] . Un mutex clasic diferă de un semafor binar prin prezența unui proprietar exclusiv, care trebuie să-l elibereze (adică să-l transfere într-o stare deblocat) [2] . Un mutex diferă de un spinlock prin trecerea controlului către planificator pentru a comuta firele de execuție atunci când mutexul nu poate fi achiziționat [3] . Există, de asemenea , blocări de citire-scriere numite mutexuri partajate care oferă, pe lângă blocarea exclusivă, o blocare partajată care permite proprietatea comună a mutexului dacă nu există un proprietar exclusiv [4] .
În mod convențional, un mutex clasic poate fi reprezentat ca o variabilă care poate fi în două stări: blocat și deblocat. Când un thread intră în secțiunea sa critică, apelează o funcție pentru a bloca mutexul, blocând firul până când mutex-ul este eliberat dacă un alt thread îl deține deja. La ieșirea din secțiunea critică, firul apelează funcția pentru a muta mutex-ul în starea deblocat. Dacă există mai multe fire de execuție blocate de un mutex în timpul deblocării, planificatorul selectează un fir de execuție pentru a relua execuția (în funcție de implementare, acesta poate fi fie un fir aleator, fie un fir determinat de anumite criterii) [5] .
Sarcina unui mutex este să protejeze obiectul de a fi accesat de alte fire decât cel care deține mutex-ul. În orice moment, doar un fir poate deține un obiect protejat de un mutex. Dacă un alt thread are nevoie de acces la datele protejate de mutex, atunci acel fir se va bloca până când mutex-ul este eliberat. Un mutex protejează datele de a fi corupte de modificări asincrone ( o condiție de cursă ), dar alte probleme, cum ar fi blocarea sau captura dublă, pot fi cauzate dacă sunt utilizate incorect .
După tipul de implementare, mutex-ul poate fi rapid, recursivsau cu controlul erorilor.
O inversare a priorității are loc atunci când ar trebui să se execute un proces cu prioritate mare, dar se blochează pe un mutex deținut de procesul cu prioritate scăzută și trebuie să aștepte până când procesul cu prioritate scăzută deblochează mutexul. Un exemplu clasic de inversare a priorității fără restricții în sistemele în timp real este atunci când un proces cu o prioritate medie se ocupă de timpul CPU, drept urmare procesul cu o prioritate scăzută nu poate rula și nu poate debloca mutex-ul [6] .
O soluție tipică a problemei este moștenirea priorității, în care un proces care deține un mutex moștenește prioritatea unui alt proces blocat de acesta, dacă prioritatea procesului blocat este mai mare decât cea a celui curent [6] .
API-ul Win32 din Windows are două implementări de mutexuri - mutexurile înșiși, care au nume și sunt disponibile pentru utilizare între diferite procese [7] , și secțiuni critice , care pot fi utilizate numai în cadrul aceluiași proces de către fire diferite [8] . Fiecare dintre aceste două tipuri de mutexuri are propriile sale funcții de captare și eliberare [9] . Secțiunea critică pe Windows este puțin mai rapidă și mai eficientă decât mutexul și semaforul, deoarece utilizează instrucțiunile de testare și setare specifice procesorului [8] .
Pachetul Pthreads oferă diverse funcții care pot fi folosite pentru a sincroniza firele [10] . Printre aceste funcții se numără și funcții pentru lucrul cu mutexuri. În plus față de funcțiile de achiziție și eliberare mutex, este furnizată o funcție de încercare de achiziție mutex care returnează o eroare dacă este de așteptat o blocare a firului. Această funcție poate fi utilizată într-o buclă de așteptare activă dacă este nevoie [11] .
Funcţie | Descriere |
---|---|
pthread_mutex_init() | Crearea unui mutex [11] . |
pthread_mutex_destroy() | Distrugerea mutexului [11] . |
pthread_mutex_lock() | Transferarea unui mutex într-o stare blocată (captură mutex) [11] . |
pthread_mutex_trylock() | Încercați să puneți mutex-ul în starea blocată și returnați o eroare dacă firul ar trebui să se blocheze deoarece mutex-ul are deja un proprietar [11] . |
pthread_mutex_timedlock() | Încercați să mutați mutex-ul în starea blocată și returnați o eroare dacă încercarea a eșuat înainte de ora specificată [12] . |
pthread_mutex_unlock() | Transferarea mutexului în starea deblocat (eliberarea mutexului) [11] . |
Pentru a rezolva probleme de specialitate, mutexurilor li se pot atribui diverse atribute [11] . Prin atribute, folosind funcția pthread_mutexattr_settype(), puteți seta tipul mutexului, care va afecta comportamentul funcțiilor de captare și eliberare a mutexului [13] . Un mutex poate fi unul din trei tipuri [13] :
Standardul C17 al limbajului de programare C definește un tip mtx_t[15] și un set de funcții pentru a lucra cu acesta [16] care trebuie să fie disponibil dacă macro- __STDC_NO_THREADS__ul nu a fost definit de compilator [15] . Semantica și proprietățile mutexurilor sunt în general în concordanță cu standardul POSIX.
Tipul mutex este determinat prin trecerea unei combinații de steaguri la funcția mtx_init()[17] :
Posibilitatea de a utiliza mutexuri prin intermediul memoriei partajate prin diferite procese nu este luată în considerare în standardul C17.
Standardul C++17 al limbajului de programare C++ definește 6 clase diferite de mutex [20] :
Biblioteca Boost oferă, în plus, mutexuri denumite și inter-procese, precum și mutexuri partajate, care permit achiziționarea unui mutex pentru proprietate partajată de mai multe fire de date de numai citire, fără excluderea scrierii pe durata achiziției de blocare, care este în esență un mecanism de blocare de citire-scriere [25] .
În cazul general, mutexul stochează nu numai starea sa, ci și o listă de sarcini blocate. Schimbarea stării unui mutex poate fi implementată folosind operații atomice dependente de arhitectură la nivel de cod de utilizator, dar la deblocarea mutex-ului, trebuie reluate și alte sarcini care au fost blocate de mutex. În aceste scopuri, o primitivă de sincronizare de nivel inferior este bine potrivită - futex , care este implementată pe partea sistemului de operare și preia funcționalitatea sarcinilor de blocare și deblocare, permițând, printre altele, crearea mutexurilor interprocese [26] . În special, folosind futex, mutex-ul este implementat în pachetul Pthreads în multe distribuții Linux [27] .
Simplitatea mutex-urilor le permite să fie implementate în spațiul utilizatorului folosind o instrucțiune de asamblare XCHGcare poate copia atomic valoarea mutex-ului într-un registru și, simultan, setează valoarea mutex-ului la 1 (scrisă anterior în același registru). O valoare mutex de zero înseamnă că este în starea blocată, în timp ce o valoare de unu înseamnă că este în starea deblocat. Valoarea din registru poate fi testată pentru 0, iar în cazul unei valori zero, controlul trebuie returnat programului, ceea ce înseamnă că mutex-ul este dobândit, dacă valoarea a fost diferită de zero, atunci controlul trebuie transferat către planificatorul să reia activitatea unui alt fir, urmat de o a doua încercare de a achiziționa mutexul, care servește ca analog al blocării active. Un mutex este deblocat prin stocarea valorii 0 în mutex folosind comanda XCHG[28] . Alternativ, LOCK BTS(implementarea TSL pentru un bit) sau CMPXCHG[29] ( implementarea CAS ) pot fi utilizate.
Transferul controlului către planificator este suficient de rapid încât să nu existe o buclă de așteptare activă, deoarece CPU-ul va fi ocupat cu executarea unui alt fir și nu va fi inactiv. Lucrul în spațiul utilizatorului vă permite să evitați apelurile de sistem care sunt costisitoare în ceea ce privește durata procesorului [30] .
Arhitectura ARMv7 folosește așa-numitele monitoare locale și globale exclusive pentru a sincroniza memoria între procesoare, care sunt mașini de stare care controlează accesul atomic la celulele de memorie [31] [32] . O citire atomică a unei celule de memorie poate fi efectuată folosind instrucțiunea LDREX[33] , iar o scriere atomică se poate face prin instrucțiunea STREX, care returnează și steag-ul de succes al operației [34] .
Algoritmul de captare mutex implică citirea valorii sale cu LDREXși verificarea valorii citite pentru o stare blocată, care corespunde valorii 1 a variabilei mutex. Dacă mutex-ul este blocat, se apelează codul de așteptare pentru eliberarea blocării. Dacă mutex-ul era în starea deblocat, atunci blocarea ar putea fi încercată folosind instrucțiunea de scriere exclusivă STREXNE. Dacă scrierea eșuează deoarece valoarea mutexului s-a schimbat, atunci algoritmul de captare se repetă de la început [35] . După capturarea mutexului, se execută instrucțiunea DMB, care garantează integritatea memoriei resursei protejate de mutex [36] .
Înainte ca mutex-ul să fie eliberat, instrucțiunea este numită și DMB, după care valoarea 0 este scrisă în variabila mutex folosind instrucțiunea STR, ceea ce înseamnă transfer în starea deblocat. După ce mutex-ul este deblocat, sarcinile de așteptare, dacă există, ar trebui să fie semnalate că mutex-ul a fost eliberat [35] .
Comunicarea intraprocesuala | |
---|---|
Metode | |
Protocoale și standarde selectate |