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;
}
::::::::::::::