Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
vuoi
o PayPal
tutte le volte che vuoi
ESERCIZIO:
Vediamo adesso di fare un esempio, per chiarire l'uso dei mutex. Presentiamo prima un programma, che
non utilizza i mutex, in cui vengono creati due thread che lavorano su una variabile intera condivisa.
Presentiamo una prima versione di tale programma, priva di sincronizzazione, proprio per evidenziare i
problemi che si possono avere quando più thread operano senza controllo su una risorsa condivisa,
ricordando che tali problemi scaturiscono dal fatto che la schedulazione dei thread avviene in una maniera
imprevedibile. *************** file: thread3.c ***************
/*lanciando l'eseguibile relativo a questo listato verrà stampato sullo standard output 22 volte il valore della
x. In particolare quello che dovrebbe accadere (se i thread fossero sincronizzati) è che i valori della x
dovrebbero andare da 0 a 10 in maniera crescente e dovrebbero essere ripetuti:
x = 0
x = 0
x = 1
x = 1
x = 2
x = 2
...
invece, proprio perché i thread non sono sincronizzati, i valori della x risultano essere ripetuti e male
intercalati, all'inizio viene stampato 2 volte lo stesso valore, alla fine potrebbe capitare che vengano stampati
dapprima valori maggiori e poi valori minori! */
#include <pthread.h>
#include<stdio.h>
int x = 0; /* Variabile condivisa, dichiarata globalmente! */
void *thread_func1(void *arg1); //prototipo della thread function
int main (void)
{ int res1, res2; //dichiaro 2 interi in cui memorizzo il valore di ritorno delle pthread_create
pthread_t IDthread1, IDthread2; //dichiaro 2 ptatori a thread
res1 = pthread_create(&IDthread1, NULL, thread_func1, NULL); //creo il primo thread
if (res1 != 0) //eseguo il controllo sulla creazione del thread
{
printf("Creazione thread1 fallita!!!\n");
return -1;
}
res2 = pthread_create(&IDthread2, NULL, thread_func1, NULL); //creo il 2° thread
if (res2 != 0)
{ printf("Creazione thread2 fallita!!!\n");
return -1;
}
pthread_join(IDthread1, NULL); //il main aspetta il primo thread
pthread_join(IDthread2, NULL); //il main aspetta il 2° thread
return (0);
} //fine funzione main
void *thread_func1(void *arg1) /*la thread function non prende in ingresso alcun parametro, tanto è vero
{ che al suo interno non viene effettuato nessun
casting*/ int i; //dichiaro un intero
long int j; //dichiaro un long int che mi serve x realizzare l'attesa mediante il for
for(i=0; i < 11; i++)
{ x = i; //questa variabile essendo dichiarata globalmente è comune ai 2 thread
for (j=0; j < 9000000; j++); //questo for serve solo x realizzare un ritardo nella
stampa! printf("x = %d\n", x); //stampo x!
}
pthread_exit(0); //il thread chiama la exit, informando il main di aver terminato!
}
eseguendo questo programma l'output sarà il seguente:
marco@ubuntu:~/Documenti/Università/Es_Sistemi_operativi/Semafori/lab_20_aprile$
x = 0
x = 1
x = 1
x = 2
x = 2
x = 3
x = 3
x = 4
x = 4
x = 5
x = 5
x = 6
x = 6
x = 7
x = 7
x = 8
x = 8
x = 9
x = 10
x = 9
x = 9
x = 10
Vediamo, adesso la versione dello stesso programma, in cui l'accesso alla variabili condivisa x è controllata
con l'ausilio di un semaforo mutex:
*************** file: Thread4.c ***************
#include <pthread.h>
#include<stdio.h>
int x = 0; /* Variabile condivisa, dichiarata globalmente! */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; /*dichiaro un semaforo globalmente, in modo
che
entrambi i thread lo possano vedere*/
void *thread_func1(void *arg1); //prototipo della thread function
int main (void)
{ int res1, res2;
pthread_t IDthread1, Idthread2;
res1 = pthread_create(&IDthread1, NULL, thread_func1, NULL); //creo il primo thread
if (res1 != 0)
{ printf("Creazione thread1 fallita!!!\n");
return -1;
}
res2 = pthread_create(&IDthread2, NULL, thread_func1, NULL); //creo il 2° thread
if (res2 != 0)
{ printf("Creazione thread2 fallita!!!\n");
return -1;
}
pthread_join(IDthread1, NULL); //il main aspetta il termine del primo thread
pthread_join(IDthread2, NULL); //il main aspetta il termine del secondo thread
return (0);
}
void *thread_func1(void *arg1) //al solito questa funzione non prende parametri in ingresso
{ int i;
long int j;
pthread_mutex_lock(&mutex); //prima di incrementare la x blocco il semaforo
/* è sempre consigliabile che la parte compresa tra il lock e l'unlock sia molto piccola e sia limitata solo alle
parti di codice in cui effettivamente c'è una manipolazione sulle variabili condivise!!!! */
for(i=0; i < 11; i++)
{ x = i; //pongo x = i
for (j=0; j < 9000000; j++); //attende
printf("x = %d\n", i); //stampo la x
}
pthread_mutex_unlock(&mutex); //sblocco il semaforo
pthread_exit(0); //il thread esce!
}
l'output del programma è il seguente:
marco@ubuntu:~/Documenti/Università/Es_Sistemi_operativi/Semafori/lab_20_aprile $ ./es4
x = 0
x = 1
x = 2
x = 3
x = 4
x = 5
x = 6
x = 7
x = 8
x = 9
x = 10
x = 0
x = 1
x = 2
x = 3
x = 4
x = 5
x = 6
x = 7
x = 8
x = 9
x = 10
eseguendo più volte lo stesso programma l'output sarà sempre lo stesso, a differenza del precedente, che
cambierà ad ogni esecuzione, in quanto diverso sarà di volta in volta lo scheduling effettuato sui thread!
Semafori generalizzati per i thread
Abbiamo presentato i semafori binari (mutex), passiamo adesso a vedere i semafori generalizzati.
Un semaforo ci fornisce metodi utili per ottenere questo meccanismo. Un semaforo sostanzialmente è un
contatore che può essere utilizzato per sincronizzare molti thread. Con i semafori generalizzati possiamo
ottenere la mutua esclusione cosi come abbiamo visto con i mutex, evitando quindi race conditions.
Ogni contatore di semaforo ha un valore, che deve essere un intero non negativo.
Nell’esempio precedente della coda in cui i job venivano processati dai thread c’è un problema: quando la
coda si svuota i thread cessano di esistere, quindi, o riempiamo preventivamente la coda, aggiungendo
processi più velocemente di quanto vengano processati, oppure possiamo implementare un meccanismo per
bloccare i threads da quando la coda si svuota fino a che nuovi jobs vengono inseriti.
Un semaforo generalizzato ci fornisce metodi utili per ottenere questo meccanismo.
Le operazioni che possono essere effettuate su un semaforo sono: Wait e Post.
Ogni semafororo deve essere assegnato ad una risorsa condivisa.
L'operazione di Wait va utilizzata nel momento in cui un thread deve accedere alla risorsa condivisa, quindi
testa il semaforo, mediante appunto l'operazione di Wait. Quello che accade è che, mediante tale
operazione, il contatore del semaforo viene decrementato di uno.
Si possono verificare però 2 casi:
1. il contatore è positivo: significa che quella risorsa è disponibile, quindi il thread decrementa di una
unità il contatore e continua la sua esecuzione, ciò significa che il thread starà impegnando una
risorsa che, quindi non sarà più disponibile, e potrà continuare nell'esecuzione delle sue operazioni.
2. Il contatore è 0: ciò significa che tutte le risorse condivise sono impegnate da altri thread, per cui
quello che succede è che il thread che ha effettuato la chiamata viene bloccato fino a che il
semaforo non torna positivo (a causa di qualche altro thread). Quando il semaforo tornerà positivo il
thread verrà risvegliato, gli verrà assegnata la risorsa e decrementato di una unità il valore del
semaforo.
L'operazione di Post, invece, consente al thread di rendere disponibile la risorsa che aveva
precedentemente impegnato, incrementando il contatore. Cioè Il thread rilascia il lock che aveva acquisito
dando la possibilità ad altri threads di ottenere il lock su quella specifica risorsa monitorata dal semaforo.
Ovviamente queste due operazioni sono svolte in maniera atomica!
GNU/Linux ci fornisce due differenti implementazioni leggermente differenti di semafori.
Quella che vedremo adesso è l’implementazione dello standard POSIX, e si usa quando bisogna fare
comunicare tra loro i threads. L’altra implementazione si utilizza per la comunicazione fra processi.
Per utilizzare un semaforo bisogna includere la libreria <semaphore.h>.
Un semaforo generalizzato viene rappresentato da una variabile del tipo sem_t, e prima di utilizzarla
dovremo inizializzarla mediante la chiamata sem_init passandole un puntatore alla variabile sem_t.
Il prototipo di tale funzione è il seguente:
int sem_init(sem_t *sem, int type, int val)
dove sem è il puntatore al semaforo da inizializzare; type indica se il semaforo è condiviso tra processi (in
linux tale opzione non viene implementata) e val è il valore iniziale del semaforo.
Per cui il secondo parametro deve essere zero ed il terzo il valore iniziale che vogliamo attribuire al
semaforo.
Tale funzione ritorna 0 se l'operazione è andata a buon fine, altrimenti ritorna -1,e viene settata la variabile
errno.
Se utilizziamo il semaforo per poco tempo è buona norma deallocarlo mediante la chiamata sem_destroy. Il
prototipo di tale funzione è: int sem_destroy(sem_t *sem)
dove sem è il puntatore al semaforo generalizzato. In seguito a questa chiamata le risorse associate al
semaforo sem vengono liberate. È possibile distruggere solamente un semaforo che è stato inizializzato
mediante la funzione sem_init(). Se si distrugge un semaforo, mediante questa chiamata, mentre altri
processi o thread ne detengono il lock produrrà un comportamento del programma non definito.
La funzione sem_destroy ritorna 0 se l'operazione va a buon fine altrimenti torna -1 e viene settata la
variabile errno.q
Le operazioni di Wait e Post si invocano con le chiamate: sem_wait e sem-post.
Wait
Il prototipo della funzione wait è: int sem_wait(sem_t *sem)
Questa funzione decrementa il valore del contatore associato al semaforo, di cui passiamo il puntatore.
Nell'ipotesi in cui il semaforo ha un valore positivo, ritorna immediatamente e l'esecuzione del thread non
viene interrotta, in caso contrario sospende il thread fino a quando qualche altro thread non rilascia una
risorsa. Anche questa funzione torna 0 in caso di successo e -1 in caso di insuccesso. Questa operazione
viene svolta in modo atomico dal sistema.
Post
Il prototipo della funzione Post è: int sem_post(sem_t *sem)
Questa funzione incrementa il valore del contatore associato al semaforo, di cui passiamo il puntatore. S