Un thread è un’unità di esecuzione all’interno di un processo. Un processo può avere più thread in esecuzione, che tipicamente condividono le risorse del processo e, in particolare, la memoria. Lo standard POSIX definisce un insieme di funzioni per la creazione e la sincronizzazione di thread. Vediamo le principali:
pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg)
prende 4 argomenti:thread
: un puntatore apthread_t
, l’analogo dipid_t
. Attenzione che non necessariamente è implementato come un intero (nelle ultime versioni di Linux è ununsigned long int
, vedipthreadtypes.h
in/usr/include/...
);attr
: attributi del nuovo thread. Se non si vogliono modificare gli attributi è sufficiente passare NULL (vederepthread_attr_init
per maggiori dettagli);start_routine
il codice da eseguire. È un puntatore a funzione che prende un puntatore avoid
e restituisce un puntatore avoid
. Ricordarsi che in C il nome di una funzione è un puntatore alla funzione;arg
eventuali argomenti da passare,NULL
se non si intende passare parametri.
pthread_exit(void *retval)
termina l’esecuzione di un thread restituendoretval
. Si noti che quando il processo termina (exit
) tutti i suoi thread vengono terminati. Per far terminare un singolo thread si deve usarepthread_exit
;pthread_join(pthread_t th, void **thread_return)
attende la terminazione del threadth
. Se ha successo, ritorna 0 e un puntatore al valore ritornato dal thread. Se non si vuole ricevere il valore di ritorno è sufficiente passareNULL
come secondo parametro (vedi esempio sotto).pthread_detach(pthread_t th)
se non si vuole attendere la terminazione di un thread allora si deve eseguire questa funzione che poneth
in stato detached: nessun altro thread potrà attendere la sua terminazione conpthread_join
e quando terminerà le sue risorse verranno automaticamente rilasciate (evita che diventino thread “zombie”). Si noti chepthread_detach
non fa sì che il thread rimanga attivo quando il processo termina conexit
.pthread_t pthread_self()
ritorna il proprio thread id.ATTENZIONE: questo ID dipende dall’implementazione ed è l’ID della libreria phread e non l’ID di sistema. Per visualizzare l’ID di sistema (quello che si osserva con il comando
ps -L
, dove L sta per Lightweight process, ovvero thread) si può usare una syscall specifica di Linuxsyscall(SYS_gettid)
.
Esempio: creazione e attesa
Vediamo un semplice esempio in cui vengono creati 2 thread che stampano il proprio id (sia quello di libreria che quello di sistema) e vanno in sleep(1)
prima di terminare. Il thread principale attende la terminazione dei due thread e poi stampa un messaggio e termina.
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <sys/syscall.h> /* For SYS_xxx definitions */ // codice dei thread. Notare che e' una funzione che prende // un puntatore e ritorna un puntatore (a void) void * codice_thread(void * a) { pthread_t tid; int ptid; tid = pthread_self(); // library tid ptid = syscall(SYS_gettid); // tid assegnato dal SO (funziona solo in Linux) printf("Sono il thread %lu (%i) del processo %i\n",tid,ptid,getpid()); sleep(1); pthread_exit(NULL); } int main() { pthread_t tid[2]; int i,err; // crea i thread (ritorna 0 quando ha successo, vedere il man!) // - gli attributi sono quelli di default (il secondo parametro e' NULL) // - codice_thread e' il nome della funzione da eseguire // - non vegnono passati parametri (quarto parametro e' NULL) for (i=0;i<2;i++) { if (err=pthread_create(&tid[i],NULL,codice_thread,NULL)) { printf("errore create [%i]\n",err); exit(EXIT_FAILURE); } } // attende i thread. Non si legge il valore di ritorno (secondo parametro NULL) for (i=0;i<2;i++) { if (err=pthread_join(tid[i],NULL)) { printf("errore join [%i]\n",err); exit(EXIT_FAILURE); } } printf("I thread hanno terminato l'esecuzione correttamente\n"); }
Il programma va compilato con l’opzione -pthread
per linkare la libreria POSIX threads e includere macro utili alla gestione dei thread.
> gcc test1.c -pthread -o test1 <=== opzione -pthread !! > ./test1 Sono il thread 140072330872576 (14335) del processo 13793 Sono il thread 140072322479872 (14336) del processo 13793 I thread hanno terminato l'esecuzione correttamente
Si possono notare gli ID di libreria (unsigned long) e tra parentesi quelli di sistema visualizzabili con ps -L
. Provare da un altro terminale mentre i thread sono in esecuzione:
$ ps -AL | grep test1 14334 14334 pts/0 00:00:00 test1 14334 14335 pts/0 00:00:00 test1 14334 14336 pts/0 00:00:00 test1 $
Esercizio 1: detach
Provare a “distaccare” uno dei thread e osservare l’errore restituito dalla join.
ATTENZIONE: essendo una libreria esterna, gli errori non possono essere visualizzati con perror
(che stampa gli errori di sistema). Consultare il manuale delle chiamate a libreria per vedere i possibili errori restituiti. In ubuntu 64 bit, i codici di errore si trovano in /usr/include/asm-generic/errno-base.h
.
Esercizio 2: somma multithreaded
Passate ai 2 thread 2 interi letti dalla linea di comando (argv[1]
e argv[2]
). I due thread calcolano il quadrato del numero intero e il thread principale, infine, stampa la somma dei due valori ottenuti.
Fare attenzione: la memoria è condivisa quindi si deve passare ai 2 thread l’indirizzo di una zona di memoria “riservata” in modo da evitare interferenze.
- Create un array di interi
num[2]
nelmain
- Copiate
atoi(argv[1])
eatoi(argv[2])
innum[0]
enum[1]
- Passate l’indirizzo di
num[0]
enum[1]
ai due thread (è necessario un cast avoid *
) - Nei thread, calcolate il quadrato e risalvatelo in
num[0]
enum[1]
(è necessario un cast aint *
) - Dopo le join il main può stampare
num[0]+num[1]
, le join infatti assicurano che i thread abbiamo già computato la somma (lasciate lasleep
per verificare che il main attende i risultati corretti)
Variante: se volete tenere input e output dei thread distinti potete usare una struct
con due campi.
// struct usata per passare i valori e ricevere i risultati struct data { int v; // input data int r; // results };
Esercizio 3: interferenze
Creare 2 thread che aggiornano ripetutamente (in un ciclo for) una variabile condivisa count
per un numero elevato di volte (ad esempio 1000000
). Stampare il valore finale per osservare eventuali incrementi perduti.
NOTA: il compilatore potrebbe ottimizzare il codice e fare, ad esempio, un unico incremento di 1000000
sulla variablie count
. In questo caso non si osserveranno interferenze. Per evitare ottimizzazioni dare a gcc l’opzione -O0
(che dovrebbe essere di default). Per sperimentare ottimizzazioni provare con -O
oppure -O3
. Per visualizzare l’assembly risultante si può usare il comando seguente:
objdump -M intel -d esercizio3 | grep -A 50 codice_thread
che fa il disassemlaggio (-d
) in stile mnemonico intel (-M intel
) dell’eseguibile a.out
. Il filtro grep cerca nel codice assembly la stringa codicethread
(nome della funzione che ci interessa) e stampa 10 righe (opzione -A 10
, after).
Dal codice assembly è abbastanza evidente come viene compilato il ciclo for.