«

»

ott 20

Sincronizzazione dei Thread e gestione delle risorse con i Mutex ( C++0X – C++11)

Come accennato nel post precedente un applicazione che sfrutta il multithreading è costituita da un insieme di processi sequenziali cooperanti, tutti in esecuzione asincrona che possono condividere dati e risorse.

L’accesso concorrente ai dati o alle risorse può provocare incoerenze nei dati o situazioni di deadlock nel caso di risorse che prevedono accesso esclusivo, in questo post cercherò di analizzare gli strumenti che C++0X offre per la sincronizzazione.

All’interno di un processo si possono separare le sezioni in cui lavora su dati interni da quelle in cui si accede a dati o risorse condivise, queste ultime vengono dette sezioni critiche e sono quelle di cui si deve curare la sincronizzazione.

Non tratterò in questo articolo le soluzioni “storiche” e full-software per la gestione delle sezioni critiche (algoritmi di Dekker, di Peterson, algoritmo del banchiere e del panettiere di Lamport, semafori in spinlock) ma tratterò l’uso di funzioni messe a disposizione dalla libreria che ci permettono di assicurare la mutua esclusione tra processi attraverso lo scheduler.

Per prima cosa cerchiamo di definire cosa deve succedere su una sezione critica: una volta individuata e minimizzata una sezione critica dobbiamo fare in modo che

  1. sia verificata la mutua esclusione ossia che soltanto un processo per volta possa trovarsi nella sezione critica
  2. si eviti l’attesa indefinita da parte di un processo che tenta di accedere a una sezione critica occupata
  3. un processo che si trova fuori dalla sezione critica non deve poter interferire con l’accesso alla sezione critica dei processi in attesa

Chi ha lavorato con i pthread in C si renderà conto che è quasi un wrapper a oggetti quello che il cpp ci offre

Il Mutex

Lo strumento fondamentale per la sincronizzazione offerto dalle stl è il Mutex, che è la crasi di mutual exlusion e serve appunto ad evitare che più processi del necessario possano accedere ad una sezione critica.
Esistono 4 tipi di mutex:

  1. semplice ( std::mutex )
  2. semplice con timeout ( std::timed_mutex )
  3. ricorsivo ( std::recursive_mutex )
  4. ricorsivo con timeout ( std::recursive_timed_mutex )
  • un mutex è semplice se il suo stato è binario ( libero/occupato ) è invece ricorsivo se permette più accessi prima di essere occupato
  • un mutex è timed se è possibile fare in modo che si liberi da solo dopo un certo tempo

fondamentalmente un mutex dispone di almeno 3 funzioni:

  • void lock();
    che permette di occuparlo o di attenderlo se è già occupato
  • void unlock();
    che permette di liberarlo
  • bool try_lock();
    che permette occuparlo se è possibile senza però rimanere in attesa

vediamo come usarli, per prima cosa costruiamo un programma che necessiti di essere sincronizzato

#include <iostream>
#include <thread>
#include <cstdlib>
#include <string>

using namespace std;

void stampa(string testo){
 for(int a=0;a<testo.length();a++){
 cout << testo[a];
 flush(cout);
 usleep( 100 * ( rand() % 900 + 100 ));
 }
 cout << endl;
}

class Worker
{
public :

 string testo;
 Worker(string t){
 testo=t;
 }
 void operator()()
 {
 stampa(testo);
 }
};

int main()
{
 thread t1(Worker("Quanto legno roderebbe un roditore se un roditore potesse rodere il legno?"));
 thread t2(Worker("How much wood could a woodchuck chuck if a woodchuck could chuck wood?"));
 t1.join();
 t2.join();
};

il codice ci farà ottenere qualcosa del tipo:

QHowu anmtuoc hl egwonodo  rcooudelrd ebbae  wuoond rocdhiutcokr cehuc ske  uinf a r odiwtooroed chpuoctkes csouled  rchoduecrek i l woloegdno??

La sincronizzazione si effettua creando un mutex globale e condividendolo tra i thread che vanno sincronizzati e racchiudendo la sezione critica dentro una lock e una unlock

...

class Worker
{
public :

 string testo;
 mutex *m;
 Worker(string t, mutex *mtx){
 m=mtx;
 testo=t;
 }
 void operator()()
 {
 m->lock();
 stampa(testo);
 m->unlock();
 }
};

int main()
{
 mutex *m= new mutex();
 thread t1(Worker("Quanto legno roderebbe un roditore se un roditore potesse rodere il legno?",m));
 thread t2(Worker("How much wood could a woodchuck chuck if a woodchuck could chuck wood?",m));
 t1.join();
 t2.join();
};

Mutex e eccezioni

Malgrado l’accesso diretto alle funzioni del mutex sia possibile è un modo pericoloso di procedere.
Infatti non gestisce le eccezioni, se un thread genera un errore all’interno della zona critica il thread viene terminato ma il mutex non viene sbloccato, quindi il modo corretto di gestire una sezione critica sarebbe usare un try{}catch in questo modo:

 void operator()()
 {
    m->lock();
    try
    {
        stampa(testo);
        m->unlock();
    }
    catch(...)
    {
        m->unlock();
        cout << "Yikes!" << endl;
    }
 }

Un modo più compatto di usare in maniera sicura i mutex si può ottenere usando gli ogetti lock_guard delle stl, che permettono anche di rilasciare il mutex in maniera automatica quando si esce dal blocco in cui sono definite ( lo sbloccano nel loro distruttore per essere precisi), il codice precedente si può riscrivere in questo modo:

 void operator()()
 {
    lock_guard<mutex> lock(*m);
    stampa(testo);
 }

Precisiamo una cosa però…

Il fatto che la guard blocchi il mutex non implica che l’intero programma non venga terminato… infatti dopo aver sbloccato il mutex propaga l’eccezione generata a livello superiore e se non c’è un catch a livello superiore questo equivale alla terminazione del processo padre

Quindi se ci limitiamo a guardare questi snippet l’esempio con la gestione manuale funziona meglio di quello con la lock ma nel caso generale l’uso della lock è comodo perché ci permette di scrivere codici per la gestione delle eccezioni che non tengono conto del mutex…spero di essermi spiegato

E con questo concludo l’articolo, nel prossimo tratterò gli altri tipi di guard e le condition usati per la gestione di situazioni più complesse

Questo post fa parte di una serie, gli altri post riguardanti il Cpp0X sono:

Share Button

2 comments

  1. whoami

    Ottimo articolo, complimenti!

  2. Vincenzo La Spesa

    Grazie :D

    Il lavoro mi porterà finalmente a lavorare di nuovo in C++ nei prossimi mesi, spero di poter aggiornare tutta la serie degli articoli allo standard C++14.

Commenti disabilitati.