APPUNTI DI SISTEMI OPERATIVI PER LE ESERCITAZIONI IN LABORATORIO
PARTE 3a - SINCRONIZZAZIONE FRA
THREADS
**********************************************SINCRONIZZAZIONE FRA THREADS
**********************************************
Per la sincronizzazione fra threads, in particolare di uno stesso processo, ci sono a
disposizione:
- semafori
- "mutex" e variabili condizione
***********************
SEMAFORI
***********************
Dei semafori definiti nello standard POSIX
(1003.1-2013) esistono
due versioni:
- I named semaphores possono essere usati da tutti i processi che conoscono il nome che li identifica a livello di sistema.
- Gli "unnamed" semaphores possono essere usati solo
da threads nello stesso processo, a meno che il semaforo non sia
collocato in una regione di memoria condivisa tra i processi.
Nel seguito verranno presi in considerazione solo i secondi, applicati per la sincronizzazione di threads di uno stesso
processo.
********************************
INIZIALIZZAZIONE E RIMOZIONE
********************************
Un semaforo POSIX unnamed si dichiara con:
sem_t s;
(tipo definito in semaphore.h); si inizializza con
sem_init(&s,0,val);
dove "val" è il valore a cui viene inizializzato; il secondo
parametro 0 serve ad indicare che il semaforo può essere usato
solo dai threads del processo che l'ha creato.
Una volta che abbiamo terminato di usare il semaforo, possiamo eliminarlo con:
sem_destroy(&s);
ma comunque, come per tutte le
variabili dichiarate in un programma, alla terminazione del processo un
tale semaforo non rimane allocato.
************************************************
OPERAZIONI DI SINCRONIZZAZIONE DOWN E UP
************************************************
L'equivalente della funzione "up" è
sem_post(&s);
e l'equivalente della "down" è
sem_wait(&s);
La funzione sem_wait ritorna subito se è possbile decrementare il
semaforo (il valore del semaforo è >0) altrimenti il thread viene sospeso
finchè:
- il valore del semaforo viene incrementato, oppure
- il processo viene interrotto da un segnale.
Il programma coda_sem_thr.c può servire a verificare in che ordine - almeno in un esempio - vengono risvegliati i thread sospesi su un semaforo. Vengono
generati 5 threads, a distanza di 1 secondo l'uno dall'altro, che si mettono
in attesa sul semaforo sem.
Successivamente, sempre a distanza di 1 secondo, vengono eseguite 5 sem_post.
Naturalmente il comportamento verificato in una o più esecuzioni non dà
certezze su quale politica sia adottata per la gestione della coda nei
semafori.
::::::::::::::
coda_sem_thr.c
::::::::::::::
#include <stdlib.h>
#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
/* verifica gestione coda semafori */
sem_t sem; /* creo semaforo posix*/
void *tf(void *p)
{
printf("Thread %lu prima della sem_wait\n",(uintptr_t)p);
sem_wait(&sem);
printf("Thread %lu dopo la sem_wait\n",(uintptr_t)p);
pthread_exit(NULL);
}
int main()
{
uintptr_t i;
pthread_t t[5];
/* inizializzo semaforo sem a 0 (setta "rosso") */
if(sem_init(&sem,0,0)==-1) {perror("sem_init"); exit(0);}
for(i=0;i<5;i++)
{ pthread_create(&t[i], NULL, tf, (void *)i);sleep(1);}
for(i=0;i<5;i++)
{sem_post(&sem);
sleep(1);
}
for(i=0;i<5;i++)
{ pthread_join(t[i], NULL);
printf("Terminato thread %lu\n",i);
}
sem_destroy(&sem);
return 0;
}
::::::::::::::
ESERCIZIO
3.1: modificare il programma "race.c" (visto in precedenza negli
appunti) che presentava il problema delle corse critiche,
utilizzando i semafori POSIX per evitare il problema. Confrontare il
tempo di esecuzione con quello del programma originale.
************************************************
ESEMPIO: PRODUTTORI E CONSUMATORI
************************************************
Il seguente esempio mostra la sincronizzazione di produttori e
consumatori. La sincronizzazione avviene come nella soluzione sul libro
di Tanenbaum, e come quella utilizza variabili globali (qui è tuttavia
esplicita la gestione del buffer).
::::::::::::::
pc_sem_thr.c
::::::::::::::
/******** N produttori, 1 consumatore con semafori e thread ********/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define N 5 /* dim. buffer */
#define NPROD 5 /* numero produttori */
#define NGIRI 20 /* quanti caratteri prodotti da ogni produttore */
struct buffer{
char pool[N];
int in,out,count;
} buf;
sem_t mutex,empty,full;
void printstatus() /* stampa contenuto del buffer */
{
int i;
printf("Contenuto del buffer:");
for (i=0;i<buf.count;i++)
putchar(buf.pool[(buf.out+i)%N]);
}
void put(char i)
{
buf.pool[buf.in]=i;
buf.in = (buf.in+1)%N;
buf.count++;
printstatus();
printf(" dopo put del carattere %c\n",i);
fflush(stdout); /* cosi' va bene anche se si ridirige l'output su disco */
}
void get(char *ip)
{
*ip=buf.pool[buf.out];
buf.out = (buf.out+1)%N;
buf.count--;
printstatus();
printf(" dopo get del carattere %c\n",*ip);
fflush(stdout); /* cosi' va bene anche se si ridirige l'output su file */
}
void *producer(void *p) /* produce NGIRI volte il carattere *p */
{
int i;
for (i=0;i<NGIRI;i++)
{
int j;
for (j=0;j<1000000;j++); /* fa finta di pensarci un po' per generare il carattere */
sem_wait(&empty);
sem_wait(&mutex);
put(*(char *)p);
sem_post(&mutex);
sem_post(&full);
}
pthread_exit(NULL);
}
void *consumer(void *filename) /* copia da buffer a filename */
{
char c;
FILE *res;
res=fopen((char *)filename,"w");
do /* esce quando trova il carattere \0 nel buffer */
{
int i;
sem_wait(&full);
sem_wait(&mutex);
get(&c);
sem_post(&mutex);
sem_post(&empty);
for (i=0;i<1000000;i++); /* fa finta di pensarci un po' per elaborare il carattere */
putc(c,res);
}
while (c!=0);
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
int i;
char c[NPROD];
pthread_t t[NPROD+1];
sem_init(&mutex,0,1);
sem_init(&empty,0,N);
sem_init(&full,0,0);
if (argc != 2)
{
fprintf(stderr,"un nome di file come argomento\n");
exit(1);
}
buf.in = buf.out = buf.count = 0;
/* genera produttori e consumatore */
fflush(stdout);
for(i=0;i<NPROD;i++)
{
c[i]='0'+i; /* il primo ha '0', il secondo '1'... */
pthread_create(&t[i], NULL, producer, &c[i]);
}
pthread_create(&t[i], NULL, consumer, argv[1]);
for(i=0;i<NPROD;i++)
pthread_join(t[i], NULL);
/* produce carattere 0 per far terminare il consumatore */
sem_wait(&empty);
sem_wait(&mutex);
put(0);
sem_post(&mutex);
sem_post(&full);
pthread_join(t[i],NULL);
return 0;
}
::::::::::::::
***********************
MUTEX E CONDIZIONI
***********************
I threads possono essere sincronizzati anche con i "mutex" e le
variabili condizione, derivate da quelle dei "monitor" per la programmazione concorrente.
Un "mutex" è la forma semplificata di semaforo sufficiente
a gestire la mutua esclusione: il "lock" di un "mutex" serve a prendere
la mutua esclusione per l'accesso a delle variabili condivise,
l'"unlock"
per rilasciarla.
Un mutex si dichiara con
pthread_mutex_t m;
si inizializza con
pthread_mutex_init(&m,NULL)
(NULL fa sì che il mutex abbia gli attributi di default).
Le operazioni di lock e unlock sono:
pthread_mutex_lock(&m);
pthread_mutex_unlock(&m);
Le variabili condizione si utilizzano per sincronizzazioni
"asimmetriche" in cui un thread segnala un evento di cui altri thread
possono essere in attesa.
Tipicamente questo evento riguarda lo stato di variabil condivise, per
cui le condizioni si usano in combinazione con i mutex.
Una variabile condizione si dichiara con
pthread_cond_t cond;
si inizializza con
pthread_cond_init(&cond,NULL)
(NULL fa sì che la condizione abbia gli attributi di default).
Un thread, tipicamente dopo avere conquistato l'accesso esclusivo
a variabili condivise con
pthread_mutex_lock(&m);
può sospendersi in attesa di "cond" con:
pthread_cond_wait(&cond,&m);
questo comporta anche l'unlock di m, cioè il rilascio della
mutua esclusione (e il nuovo lock di m quando il thread verrà
sbloccato).
Un thread può "segnalare" una condizione con:
pthread_cond_signal(&cond);
che risveglia uno dei threads bloccati su cond (se non ce ne sono, non
ha effetto).
Esiste anche una
pthread_cond_broadcast(&cond);
che risveglia tutti i threads bloccati sulla condizione.
Tipicamente, la segnalazione viene fatta da un thread che opera sulle
stesse variabili condivise e quindi ha il lock sul mutex che il
processo bloccato ha indicato come secondo parametro della propria
"wait" (anzi
è raccomandato che questo avvenga sempre e non solo
"tipicamente",
per evitare il problema della "perdita della segnalazione".
Il thread sbloccato, o i thread sbloccati in caso di "broadcast",
potrà (potranno) riprendere il lock (uno per volta) quando il
processo
segnalante lo rilascerà.
ESERCIZIO 3.2: modificare il programma "race.c" che presentava il
problema delle corse critiche, utilizzando i mutex per evitare il
problema.
Confrontare il costo in tempo con quello dell'esercizio 3.1.
************************************************
ESEMPIO: PRODUTTORI E CONSUMATORI
************************************************
Il seguente esempo illustra la realizzazione della sincronizzazione
di produttori e consumatori come nell'esempio precedente, ma con
mutex e condizioni. Anche in questo caso la soluzione è simile a quella
sul testo per i "monitor", nella versione presentata a lezione per
mutex e condizioni dei Pthread.
::::::::::::::
pc_cond_thr.c
::::::::::::::
/******** N produttori, 1 consumatore con condizioni e thread ********/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define N 5 /* dim. buffer */
#define NPROD 5 /* numero produttori */
#define NGIRI 20 /* quanti caratteri prodotti da ogni produttore */
struct buffer{
char pool[N];
int in,out,count;
} buf;
pthread_mutex_t m;
pthread_cond_t empty,full;
void printstatus() /* stampa contenuto del buffer */
{
int i;
printf("Contenuto del buffer:");
for (i=0;i<buf.count;i++)
putchar(buf.pool[(buf.out+i)%N]);
}
void put(char i)
{
buf.pool[buf.in]=i;
buf.in = ((buf.in)+1)%N;
buf.count++;
printstatus();
printf(" dopo put del carattere %c\n",i);
fflush(stdout); /* cosi' va bene anche se si ridirige l'output su disco */
}
void get(char *ip)
{
*ip=buf.pool[buf.out];
buf.out = ((buf.out)+1)%N;
buf.count--;
printstatus();
printf(" dopo get del carattere %c\n",*ip);
fflush(stdout); /* cosi' va bene anche se si ridirige l'output su disco */
}
void *producer(void *p) /* produce NGIRI volte il carattere *p */
{
int i;
for (i=0;i<NGIRI;i++)
{
int j;
for (j=0;j<1000000;j++); /* fa finta di pensarci un po' per generare il carattere */
pthread_mutex_lock(&m);
while (buf.count==N) pthread_cond_wait(&empty,&m);
put(*(char *)p);
pthread_cond_signal(&full);
pthread_mutex_unlock(&m);
}
pthread_exit(NULL);
}
void *consumer(void *filename) /* copia da buffer a filename */
{
char c;
FILE *res;
res=fopen((char *)filename,"w");
do /* esce quando trova il carattere \0 nel buffer */
{
int i;
pthread_mutex_lock(&m);
while (buf.count==0) pthread_cond_wait(&full,&m);
get(&c);
pthread_cond_signal(&empty);
pthread_mutex_unlock(&m);
for (i=0;i<1000000;i++); /* fa finta di pensarci un po' per elaborare il carattere */
putc(c,res);
}
while (c!=0);
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
int i;
char c[NPROD];
pthread_t t[NPROD+1];
pthread_mutex_init(&m,NULL);
pthread_cond_init(&empty,NULL);
pthread_cond_init(&full,NULL);
if (argc != 2)
{
fprintf(stderr,"un nome di file come argomento\n");
exit(1);
}
buf.in = buf.out = buf.count = 0;
/* genera produttori e consumatore */
fflush(stdout);
for(i=0;i<NPROD;i++)
{
c[i]='0'+i;
pthread_create(&t[i], NULL, producer, &c[i]);
}
pthread_create(&t[i], NULL, consumer, argv[1]);
for(i=0;i<NPROD;i++)
pthread_join(t[i], NULL);
/* produce carattere 0 per far terminare il consumatore */
pthread_mutex_lock(&m);
while (buf.count==N) pthread_cond_wait(&empty,&m);
put((char)0);
pthread_cond_signal(&full);
pthread_mutex_unlock(&m);
pthread_join(t[i],NULL);
return 0;
}