Mutex
Un mutex (abreviere de la "mutual exclusion") este o primitivă de sincronizare prin care putem proteja accesul la date atunci când avem scrieri (potențial) concurente. El funcționează ca un "zăvor" ce protejează accesarea unor resurse partajate.
Un mutex se folosește pentru a delimita o regiune critică, adică o zonă a programului în care se poate afla cel mult un thread la un moment dat de timp. Dacă un thread T1 încearcă să intre într-o regiune critică atunci când alt thread T0 este deja acolo, T1 se va bloca până când T0 va ieși din regiunea critică.
În cadrul exemplului de mai sus, am putea folosi un mutex pentru a defini o regiune critică în jurul operației de incrementare a lui a, lucru care ar face imposibilă intercalarea operațiilor celor două thread-uri. Primul thread care intră în regiunea critică va incrementa a în mod exclusiv la 2, iar cel de-al doilea thread nu va putea incrementa a decât atunci când el este deja 2.
Un mutex are două operații principale: închidere (lock) și deschidere (unlock). Prin închidere, un thread marchează intrarea în zona critică, adică specifică faptul că orice alt thread care va încerca să facă o operație de închidere va trebui să aștepte. Prin deschidere, se marchează ieșirea din zona critică și deci permisiunea ca un alt thread să intre în zona critică.
În Pthreads, o secvență tipică de folosire a unui mutex arată în felul următor:
- se creează și se inițializează o variabilă de tip mutex
- mai multe thread-uri încearcă să închidă mutexul (adică să intre în zona critică)
- unul singur dintre ele reușește acest lucru și ajunge să "dețină" mutexul (adică se află în regiunea critică)
- thread-ul aflat în zona critică realizează diverse operații pe datele protejate
- thread-ul care deține mutexul iese din zona critică (deschide mutexul)
- alt thread intră în zona critică și repetă procesul
- la final, variabila de tip mutex este distrusă.
În Pthreads, un mutex se reprezintă printr-o variabilă de tip pthread_mutex_t, și se inițializează folosind următoarea funcție:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
Primul parametru reprezintă o referință la variabila mutex, iar al doilea parametru specifică atributele mutexului nou-creat (dacă se dorește un comportament implicit, parametrul attr se poate lăsa NULL).
Pentru a dezaloca un mutex, se folosește următoarea funcție, care primește ca parametru un pointer la mutexul care urmează a fi dezalocat:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Pentru a se face lock pe un mutex, se folosește următoarea funcție, care primește ca parametrul mutexul:
int pthread_mutex_lock(pthread_mutex_t *mutex);
Operația inversă, prin care se specifică ieșirea dintr-o zonă critică (adică deschiderea mutexului), este executată prin intermediul următoarei funcții:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
Toate patru funcțiile de mutex returnează 0 dacă s-au executat cu succes, sau un cod de eroare în caz contrar.
O reprezentare grafică a funcționării unui mutex se poate vedea în figura de mai jos, într-un scenariu în care avem două thread-uri (T0 și T1) și o regiune critică controlată de mutex (chenarul negru din imagine). La momentul de timp 1 (în partea stângă a figurii), T0 încearcă să intre în regiunea critică. Pentru că, la momentul respectiv de timp, niciun alt thread nu deține mutexul (adică nu se află în regiunea critică), T0 intră în regiunea critică (momentul de timp 2). Mai departe, atunci când T1 ajunge la intrarea în regiunea critică (încearcă să facă lock pe mutex) la momentul de timp 3, se blochează pentru că mutexul este curent deținut de T0 (acesta se află în regiunea critică). Abia în momentul în care T0 a ieșit din zona critică (la momentul de timp 4), T1 se va putea debloca și își va continua execuția.
Dacă vrem să protejăm o secțiune din programul nostru folosind un mutex, atunci fiecare thread care accesează acea secțiune trebuie să facă lock și unlock pe aceeași variabilă mutex. De asemenea, dacă un thread vrea să facă unlock pe un mutex pe care nu îl deține (nu a făcut lock pe el în prealabil), va rezulta un comportament nedefinit.