I Thread POSIX

I Thread POSIX

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.

Creazione Thread

pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg)

Exit e Join

pthread_exit(void *retval)
termina l’esecuzione di un thread restituendo retval. Si noti che quando il processo termina (exit) tutti i suoi thread vengono terminati. Per far terminare un singolo thread si deve usare pthread_exit;

pthread_join(pthread_t th, void **thread_return) attende la terminazione del thread th. Se ha successo, ritorna 0 e un puntatore al valore ritornato dal thread. Se non si vuole ricevere il valore di ritorno è sufficiente passare NULL come secondo parametro.

Detach e self

pthread_detach(pthread_t th)
se non si vuole attendere la terminazione di un thread allora si deve eseguire questa funzione che pone th in stato detached: nessun altro thread potrà attendere la sua terminazione con pthread_join e quando terminerà le sue risorse verranno automaticamente rilasciate (evita che diventino thread “zombie”).
Si noti che pthread_detach non fa sì che il thread rimanga attivo quando il processo termina con exit.

pthread_t pthread_self() ritorna il proprio thread id.

ATTENZIONE: questo è l’ID della libreria phread e non l’ID di sistema. Per visualizzare l’ID di sistema si può usare, in Linux, syscall(SYS_gettid).

Esempio

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>   /* For SYS_xxx definitions */

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!)
    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");
}

Esempio

Il programma va compilato con l’opzione -pthread per aggiungere macro e codice utile alla gestione dei thread.

> gcc test1.c -pthread -o test1	    <=== opzione -pthread !! 
> ./test1 
Sono il thread 139816621209344 (221) del processo 220
Sono il thread 139816612816640 (222) del processo 220
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 (o lanciando test1 &) mentre i thread sono in esecuzione:

$ ps -AL | grep test1
  220   220 pts/0    00:00:00 test1
  220   221 pts/0    00:00:00 test1
  220   222 pts/0    00:00:00 test1

Esercizio 1

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 parallela

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] nel main
  • Copiate atoi(argv[1]) e atoi(argv[2]) in num[0] e num[1]
  • Passate l’indirizzo di num[0] e num[1] ai due thread (è necessario un cast a void *)
  • Nei thread, calcolate il quadrato e risalvatelo in num[0] e num[1] (è necessario un cast a int *)
  • Dopo le join il main può stampare num[0]+num[1], le join infatti assicurano che i thread abbiamo già computato la somma (lasciate la sleep 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.

Esercizio 3: interferenze

Creare 2 thread che aggiornano ripetutamente (in un ciclo for) una variabile condivisa count, dichiarata fuori dalla funzione main, per un numero elevato di volte (ad esempio 1000000). Stampare il valore finale, dopo le join, per osservare eventuali incrementi perduti dovuti a interferenze.

NOTA: il compilatore potrebbe ottimizzare il codice e fare, ad esempio, un unico incremento di 1000000 sulla variabile count o incrementare count senza usare registi! In questi casi si potrebbero perdere direttamente 1000000 incrementi o non non avere interferenze! Per evitare ottimizzazioni dare a gcc l’opzione -O0 (che dovrebbe essere di default). Per sperimentare ottimizzazioni provare con -O oppure -O3.