APPUNTI DI SISTEMI OPERATIVI 1 PER LE ESERCITAZIONI IN LABORATORIO
PARTE 2D - POSIX THREADS
La libreria dei POSIX threads - pthreads in breve - è
disponibile su molte versioni di Unix.
Il funzionamento dei programmi che utilizzano le funzioni della
libreria dovrebbe essere lo stesso su versioni diverse di Unix (sarebbe
questo lo scopo di uno "standard" come POSIX) anche se in realtà
questo non è completamente vero, a causa delle diverse scelte
implementative
e di conseguenza del fatto che una implementazione può
supportare
soltanto una parte dello standard.
Un riferimento generale per i Posix Threads è il testo di
Butenhof, "Programming with POSIX threads" (Addison-Wesley 1997).
***************************
COMPILAZIONE E LINKING
***************************
Per usare la libreria dei pthreads si include <pthread.h> e
si passa al comando di collegamento (o compilazione e collegamento)
l'opzione "-pthread" dopo i file.
*****************************
CREAZIONE, TERMINAZIONE
*****************************
I pthreads hanno un identificatore di tipo "pthread_t", e degli
attributi di tipo "pthread_attr_t".
Entrambi questi tipi sono "opachi" cioè non bisogna fare
assunzioni su come siano rappresentati; vanno utilizzati soltanto con
le funzioni apposite, e non mediante assegnazioni.
Gli attributi di un thread comprendono la dimensione della stack e
attributi usati per lo scheduling.
Con:
int pthread_attr_init(pthread_attr_t *attr);
si inizializza, con gli attributi di default, un "contenitore di
attributi" *attr, da utilizzare in seguito nella funzione di creazione
di un nuovo thread:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void*),void *arg);
crea un nuovo thread nel processo chiamante.
L'identificatore del nuovo thread viene messo in "*thread", il thread
viene creato con gli attibuti "*attr" (si può anche usare NULL
come secondo argomento, per creare un thread con gli attributi di
default)
e va ad eseguire la funzione "*start_routine", a cui viene passato
l'argomento "arg".
La funzione da eseguire deve avere un puntatore generico ("void *")
come unico argomento, e valore restituito dello stesso tipo.
Un thread termina quando termina di eseguire la sua "start_routine"
oppure quando chiama:
void pthread_exit(void *value_ptr);
Un "valore di ritorno" del thread, che nel primo caso è il
valore di ritorno della "start_routine", nel secondo quello passato a
pthread_exit, viene reso disponibile al thread che chiama:
int pthread_join(pthread_t thread, void **value_ptr);
La chiamata di pthread_join sospende il thread chiamante fino alla
terminazione del thread specificato.
Si veda il seguente esempio.
::::::::::::::
t1.c
::::::::::::::
#include <pthread.h>
#include <stdio.h>
void *tbody(void *arg)
{
int j;
printf(" Thread due\n");
*(int *)arg = 10;
for (j=0;j<1000000000;j++); /* per vedere che chi fa join aspetta */
pthread_exit(NULL); /* oppure return(NULL); */
}
int main(int argc, char *argv[])
{
int i;
pthread_t t;
void *result;
pthread_create(&t, NULL, tbody, (void *) &i);
/* e' equivalente dichiarare pthread_attr_t tattr; e chiamare
pthread_attr_init(&attr);
pthread_create(&t, &tattr, tbody, &i);
se invece si vogliono usare attributi diversi da
quelli di default, li si modificano tra attr_init e create */
printf("Thread uno \n");
pthread_join(t, &result);
if (result == NULL) {
printf("i: %d \n",i);
return 0;
}
else return 1;
}
::::::::::::::
ESERCIZIO 2.12: compilare il programma precedente :-) . Se il primo tentativo non ha successo,
interpretare quanto si vede scritto e rivedere l'inizio di questo
capitolo di appunti.
L'esempio evidenzia che la memoria (la variabile i, in particolare) è condivisa. Alla funzione eseguita dal
thread viene passato un puntatore alla variabile i, dichiarata nel main.
Attenzione però: in questo esempio la funzione chiamante attende
la terminazione del thread prima di terminare, ma, in generale, passare
ad un thread puntatori a variabili sulla stack può dar luogo a
errori se non c'è garanzia che la funzione in cui la variabile
è dichiarata non termini prima del thread.
Anche il fatto che le variabili globali siano condivise può dar luogo ad
errori di programmazione (es. una variabile usata da un thread T1 per
uno scopo e contemporaneamente da un altro T2 per un altro scopo). Per
questo motivo esiste la
possibilità di allocare dati "privati" di un thread ma "globali"
nel senso che sono utilizzabili da più funzioni ("thread
specific data"), non trattati in questi appunti.
La condivisione di memoria può dar luogo alle cosiddette corse critiche di cui si tratta più avanti.
Un buon motivo per avere diversi thread in un processo è per
far sì che quando uno di questi effettua una operazione sospensiva,
possa essere
data la
CPU a threads pronti dello stesso processo.
Questo è illustrato nell'esempio seguente, in cui si può
notare che quando il thread principale si blocca in attesa di leggere
un carattere, il secondo thread, come ci si aspetta, procede.
::::::::::::::
t2.c
::::::::::::::
#include <pthread.h>
#include <stdio.h>
void *tbody(void *arg)
{
int m,n;
for (m=0;m<20;m++)
{
for (n=0;n<200000000;n++);
printf("working\n");
}
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
pthread_t t;
void *status;
pthread_create(&t, NULL, tbody, NULL);
printf("un carattere, prego\n");
getchar(); /* il thread si sospende */
printf("visto il carattere\n");
pthread_join(t, &status);
return 0;
}
::::::::::::::
Il
seguente esempio (simile a par.c per i processi) può essere usato
per verificare che in Linux, su una macchina con più CPU, thread diversi vengono fatti avanzare assegnando ad essi le CPU.
Nel
programma viene creato un numero di thread passato come argomento, ogni
thread consuma CPU scrivendo ogni tanto una stringa, nella quale i vari
thread sono identificati da un lettera "a" "b", etc.
ESERCIZIO 2.13. Verificare che la macchina sia "scarica", cioè che sia basso
(vicino a 0) il "carico" medio (load average) recente. Il carico è il
numero di thread pronti o in esecuzione e la media di tale valore negli ultimi 1, 5 e
15 minuti viene visualizzata dai comandi "w" e "top".
Lasciando girare "top"
su una finestra, su un'altra chiamare l'eseguibile con
"time" passando valori crescenti dell'argomento (es. "time ./t3 1", poi
"time ./t3 2") e interpretare il risultato, anche in base alla %CPU
visualizzata da "top" per t3.
::::::::::::::
t3.c
::::::::::::::
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void *tbody(void *arg)
{
int i,j;
for (j=0;j<10;j++) {
for (i=0; i< 500000000; i++);
printf("Thread %c %d\n",*(char *)arg,j);
}
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
pthread_t t[10];
void *status;
char *a;
int i,n;
if (argc!=2) { fprintf(stderr,"Passare un argomento da 1 a 10\n"); exit(1);};
n=atoi(argv[1]);
if (n<1 || n >10) { fprintf(stderr,"Passare un argomento da 1 a 10\n"); exit(1);};
a=(char *)malloc(11);
strcpy(a,"abcdefghij");
for(i=0;i<n;i++)
pthread_create(&t[i], NULL, tbody, (void *)&a[i]);
for(i=0;i<n;i++) pthread_join(t[i], &status);
return 0;
}
::::::::::::::
L'esempio seguente evidenzia i problemi di "corse critiche" con le
variabili condivise.
La variabile n è inizializzata a 10; un thread la
incrementa un numero k di volte, passato come argomento al programma;
l'altro thread la decrementa per lo stesso numero di volte.
Ci si
potrebbe aspettare che al termine la variabile valesse nuovamente 10,
tuttavia, per k
sufficientemente grande la variabile finisce per avere valori anche
molto diversi. Nella parte del corso sulla sincronizzazione di processi
e threads e programmazione concorrente si studia come risolvere
questi problemi regolando il traffico nell'accesso a dati condivisi.
::::::::::::::
race.c
::::::::::::::
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int k;
int n=10;
void *tbody(void *arg)
{
int j;
for (j=0;j<k;j++) n++;
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
int j;
pthread_t t;
if (argc!=2) {
fprintf(stderr,"Chiamare con un argomento numerico\n");
exit(1);
}
k = atoi(argv[1]);
pthread_create(&t, NULL, tbody, NULL);
for (j=0;j<k;j++) n--;
pthread_join(t, NULL);
printf(" n = %d \n",n);
return 0;
}
::::::::::::::