MUTEX Prof.ssa Sara Michelangeli Quando si programma in modalità concorrente bisogna gestire le problematiche di accesso alle eventuali sezioni critiche. Una sezione critica è una sequenza di istruzioni in cui un thread accede a risorse comuni ad altri thread concorrenti. In particolare, quando una sezione critica prevede modifiche di variabili condivise, è necessario attuare una protezione che garantisca la mutua esclusione, cioè il divieto di accesso contemporaneo alla risorsa comune. Per i Thread POSIX la protezione delle sezioni critiche può essere ottenuta con una variabile mutex (Mutual Exclusion). Una variabile mutex è un semaforo binario che ha valore 0 (occupato), 1 (libero). Un mutex, posto a protezione di una sezione critica, previene la race condition (interferenza): dà l'accesso all'esecuzione ad un solo thread alla volta, lasciando fuori gli altri thread, fino a che il thread che sta occupando la sezione non ne ha terminato l'esecuzione. Per creare un mutex è necessario usare una variabile di tipo pthread_mutex_t, sempre di libreria pthread, che contiene informazioni sul nome del mutex, il proprietario, la struttura associata al mutex, la coda dei processi sospesi in attesa che mutex sia libero ecc. La dichiarazione ha la sintassi pthread_mutex_t semaforo; L'inizializzazione di una variabile mutex puo' essere statica o dinamica. L'inizializzazione statica avviene con l'uso della macro PTHREAD_MUTEX_INITIALIZER. L'istruzione pthread_mutex_t semaforo = PTHREAD_MUTEX_INITIALIZER; inizializza la variabile semaforo a occupato L'inizializzazione dinamica si attua con l'uso della funzione init il cui prototipo è int pthread_mutex_init( pthread_mutex_t *mutex, pthread_mutex_attr_t *mattr ) i cui parametri sono un puntatore alla variabile mutex e gli attributi del semaforo che, al valore NULL di default, inizializzano il semaforo a libero. L'istruzione pthread_mutex_init(&semaforo,null); inizializza la variabile semaforo a libero. Le funzioni base che operano su mutex e che consentono la gestione della mutua esclusione, l'attività più impegnativa per chi si occupa di programmazione concorrente, sono le due funzioni di lock e unlock. La funzione di blocco ha prototipo int pthread_mutex_lock(pthread_mutex_t * semaforo) accetta un singolo semaforo, e, posta all'inizio della sezione critica ha lo scopo di sospendere l'accesso alla sezione per tutti i thread concorrenti a quello in esecuzione, fino allo sblocco della stessa.quando vi è l'istruzione pthread_mutex_lock(&semaforo); solo il thread che è riuscito ad eseguire la lock prosegue l esecuzione e puo' accedere ai dati, gli altri thread chiamanti rimangono bloccati, vanno in attesa. Come si vede nel prototipo la lock è una funzione che restituisce un intero, uno zero, in caso di successo, o un codice di errore diverso da zero in caso di fallimento. Esiste anche una variante della funzione lock, la trylock, non bloccante per il thread chiamante. La funzione di sblocco ha prototipo int pthread_mutex_unlock(pthread_mutex_t * semaforo)
ed ha l'effetto di liberare la variabile mutex. L'istruzione pthread_mutex_unlock(&semaforo) posta al termine della sezione critica sblocca il semaforo. A questo punto un altro thread, che ha precedentemente chiamato la lock della mutex, in coda per accedere alla sezione critica, viene svegliato potrà terminare la lock (bloccando a sua volta l'accesso ad altri concorrenti) e procedere con l'esecuzione. Il valore di ritorno della funzione è analogo a quello della lock. E' possibile eliminare un mutex con la funzione int pthread_mutex_destroy( pthread_mutex_t *mutex ) che ha come parametro il puntatore al thread da distruggere e ritorna un valore 0 in caso di successo Condition Variable Come abbiamo appena visto i mutex hanno un importantissimo compito, ma, il prezzo, in termini di efficienza del programma potrebbe essere molto alto e, di fatto, rendere inutile la concorrenza. Infatti i threads in attesa dello sblocco si potrebbero trovare impegnati in onerose attività di busypolling (sequenze periodiche di test per il verificarsi di eventi). Per questo l'uso dei mutex viene spesso associato alle condition variables, variabili che consentono la sincronizzazione tra thread che condividono un mutex, e che permettono ai threads di sospendere la propria esecuzione in attesa del verificarsi di determinate condizioni su dati condivisi. E' da evitare l'uso di condition variables senza mutex, per scongiurare pericolose situazioni di deadlock. Inoltre è opportuno ribadire che una variable condition senza mutex non assicura mutua esclusione. Per creare una condition variable è necessario usare una variabile di tipo pthread_cond_t, sempre di libreria pthread. La dichiarazione ha la sintassi pthread_cond_t cond; Come per i mutex anche l'inizializzazione di una variable condition puo' essere statica o dinamica, statica con l'istruzione pthread_cond_t cond = PTHREAD_COND_INITIALIZER; e dinaminca con l'uso della funzione init int pthread_cond_init( pthread_cond _t *cond, pthread_cond_attr_t *condattr ) i cui parametri sono un puntatore alla variabile cond e gli attributi della variabile di condizione, al valore NULL di default. Le funzioni base che operano con variable condition sono la sospensione, wait, per mettere un thread in attesa, e il risveglio, signal, per risvegliare i thread sospesi La funzione di per mettere in attesa un thread è pthread_cond_wait(pthread_cond_t cond, pthread_mutex_t * semaforo) ha come parametri la variabile di condizione e il mutex che regola l'accesso alla sezione critica. Questa funzione blocca il thread chiamante sulla coda associata a cond, e il mutex semaforo viene sbloccato in modo da liberare la sezione critica ad un altro thread. Questa funzione deve essere chiamata quando il mutex è bloccato. La funzione di per risvegliare un thread bloccato è pthread_cond_signal(pthread_cond_t cond) il thread che viene svegliato è il primo della coda dei sospesi, se non vi è nessun processo in attesa la funzione viene ignorata. Per risvegliare tutti i thread in attesa si usa un funzione di broadcast pthread_cond_broadcast(pthread_cond_t cond)
che si rivolge a tutti i thread in attesa su quella condizione. Esempio Mettiamo in pratica i concetti espressi con un esempio classico della sincronizzazione, il problema produttore-consumatore. Si parla di problema produttore-consumatore quando due sezioni di un programma lavorano in concorrenza con compiti specifici: una produce un dato, l'altra lo consuma, in una alternanza ciclica, e, naturalmente, devono essere sincronizzate tra loro. Pensiamo ad esempio ad una variabile condivisa, con un thread che si occupa di acquisirla da input e un altro di stamparla. Il consumatore non deve stampare finchè il produttore non ha letto da input il dato, ma il produttore non deve leggere finchè il consumatore non ha stampato il dato precedente. Implementiamo un primo codice privo di mutex e variable condition #include<stdio.h> #include <pthread.h> int variabilecondivisa; //variabile che dovrebbe essere utilizzata in mutua esclusione //thread di acquisizione: produttore void* threadacquisisci(void* arg) while (1) //ciclo infinito printf(" \n"); scanf("%d",&variabilecondivisa); fflush(stdin); pthread_exit(null); //thread di stampa :consumatore void* threadstampa (void* arg) while(1) //ciclo infinito sleep(3);//attesa printf ("dato stampato è %d \n",variabilecondivisa); pthread_exit(null); int main() pthread_t miot;//thread per produrre il dato pthread_t miot2;//thread per stampare il dato //creazione dei thread pthread_create(&miot,null,threadacquisisci,null); pthread_create(&miot2,null,threadstampa,null); //attendo terminazione thread pthread_join(miot,null); pthread_join(miot2,null); //fine programma printf("programma terminato");
L'esecuzione darà risultati del tipo 3 4 dato stampato è 4 6 dato stampato è 6 8 ecc. E' evidente che l'uso della sleep non è in alcun modo sufficiente a sincronizzare i due thread, si origina una sequenza chiaramente errata. Inoltre se smettiamo di inserire vediamo che il consumatore continua a eseguire codice, stampando più volte l'ultimo dato acquisito Ora correggiamo il programma garantendo la sincronizzazione e la mutua esclusione con l'uso di mutex e variable condition. #include<stdio.h> #include <pthread.h> int variabilecondivisa; //variabile in mutua esclusione //mutex per la mutua esclusione identificatore semaforo pthread_mutex_t semaforo; //variable condition per la sincronizzazione identificatore empty pthread_cond_t empty; //variable condition per la sincronizzazione identificatore empty pthread_cond_t full; //thread di acquisizione: produttore void* threadacquisisci(void* arg) while(1) //attesa del segnale di utilizzo del dato precedente //non si deve leggere finchè non è avvenuta la stampa precedente pthread_cond_wait(&empty,&semaforo); //lock per l'accesso alla sezione critica //il dato deve essere letto non puo' ancora essere prelevato pthread_mutex_trylock(&semaforo); printf(" \n"); scanf("%d",&variabilecondivisa); fflush(stdin);
//invio segnale risveglio consumatore: il nuovo dato è stato prodotto pthread_cond_signal(&full); //unlock per uscita dalla sezione critica //il dato puo' essere prelevato pthread_mutex_unlock(&semaforo); pthread_exit(null); //thread di stampa :consumatore void* threadstampa (void* arg) //per la prima produzione pthread_cond_signal(&empty); while(1) //attesa del segnale del produttore //non si deve stampare finchè non è avvenuta la lettura pthread_cond_wait(&full,&semaforo); //lock per l'accesso alla sezione critica //il dato prima di una nuova produzione deve essere stampato pthread_mutex_trylock(&semaforo); printf ("dato stampato è %d \n",variabilecondivisa); //unlock per uscita dalla sezione critica //il dato puo' essere scritto pthread_mutex_unlock(&semaforo); //sveglia il thread produttore in attesa pthread_cond_signal(&empty); pthread_exit(null); int main() pthread_t produttore;//thread per produrre il dato pthread_t consumatore;//thread per stampare il dato pthread_mutex_init(&semaforo,null);//inizializzo semaforo a libero pthread_cond_init(&empty, NULL); pthread_cond_init(&full, NULL); //creazione dei thread pthread_create(&produttore,null,threadacquisisci,null); pthread_create(&consumatore,null,threadstampa,null); //attendo terminazione thread pthread_join(produttore,null); pthread_join(consumatore,null); //fine programma printf("programma terminato");
L'esecuzione sarà del tipo 3 dato stampato è 3 4 dato stampato è 4 1 3 dato stampato è 3 4 dato stampato è 4 1 dato stampato è 1 5 dato stampato è 5 6 dato stampato è 6 7 dato stampato è 7 9 dato stampato è 9 dato stampato è 1 5 dato stampato è 5 6 dato stampato è 6 7 dato stampato è 7 9 dato stampato è 9 cioè correttamente sincronizzata.