24 Jun 2023, 8:32 PM
24 Jun 2023, 8:32 PM

10. Threads

I thread sono singole sequenze di esecuzione all’interno di un processo, aventi alcune delle proprietà dei processi. I threads non sono indipendenti tra loro e condividono il codice, i dati e le risorse del sistema assegnate al processo di appartenenza. Come ogni singolo processo, i threads hanno alcuni elementi indipendenti, come lo stack, il PC ed i registri del sistema.

La creazione di threads consente un parallelismo delle operazioni in maniera rapida e semplificata. Context switch tra threads è rapido (il sistema operativo conserva lo stato del processo o thread in modo da poter essere ripreso in un altro momento), così come la loro creazione e terminazione. Inoltre, la comunicazione tra threads è molto veloce.

Per la compilazione è necessario aggiungere il flag -pthread, ad esempio:

gcc -o program main.c -pthread

Threads in C

In C i thread corrispondono a delle funzioni eseguite in parallelo al codice principale. Ogni thread è identificato da un ID e può essere gestito come un processo figlio, con funzioni che attendono la sua terminazione.

Sebbene da un punto di vista l'esecuzione di diversi thread sia sempre parallela, a causa delle politiche di scheduling delle CPU, l'esecuzione può essere parallela solo se la CPU la supporta, ossia se dispone di più core.

Creare un thread

int pthread_create
(
	pthread_t *restrict thread, // puntatore a una variabile di tipo pthread_t che conterrà l’ID del thread creato
	const pthread_attr_t *restrict attr, //un puntatore a una struttura pthread_attr_t che specifica gli attributi del thread (come la dimensione dello stack, lo stato di detach, la politica di scheduling e la priorità di scheduling). Se questo parametro è impostato su NULL, verranno utilizzati gli attributi predefiniti.
	void *(*start_routine)(void *), //un puntatore alla funzione che il thread eseguirà. Questa funzione deve avere un parametro di tipo void * e restituire un valore di tipo void *
	void *restrict arg //un puntatore ai parametri da passare alla funzione specificata dal parametro start_routine
);

Quando si crea un nuovo thread, è necessario fornire un puntatore pthread_t, che verrà riempito con il nuovo ID generato. attr consente di modificare il comportamento dei thread, mentre start_routine serve a definire quale funzione deve essere eseguita dal thread. arg è un puntatore void che può essere utilizzato per passare qualsiasi argomento sia richiesto. NB: void* è il tipo di variabile più grande e può quindi essere usato per puntare a qualsiasi struttura di dati.

Esempio creazione

#include <stdio.h> 
#include <pthread.h>
#include <unistd.h> //threadCreate.c
void * my_fun(void * param)
{
	printf("This is a thread that received %d\n", *(int *)param); //Il puntatore a void * viene riconvertito in un puntatoe a int
	return (void *)3;
}

int main(void)
{
	pthread_t t_id; //Questa variabile conterrà l'ID del thread creato
	int arg=10;
	// We need to cast the augment to a void *. We are passing the address of the variable!
	pthread_create(&t_id, NULL, my_fun, (void *)&arg); //Funzione per creare un nuovo thread. Il primo parametro è un puntatore alla variabile t_id, il secondo è impostato su NULL per utilizzare hli attributi predefiniti del thread ed il terzo è un puntatore alla funzione my_fun. Il quarto è un puntatore alla variabile arg, che deve essere convertito in un puntatore a void * perché questa funzione accetta un puntatore generico come parametro e un puntatore a void * può essere utilizzato per puntare a qualsiasi tipo di dato e quindi consente di passare qualsiasi tipo di parametro alla funzione del thread (il puntatore a int viene convertito a void in questo caso)
	printf("Executed thread with id %ld\n",t_id); //Stampa l'ID del thread creato
	sleep(3); //Attende 3 secondi prima di terminare
}

Attenzione al parametro

#include <stdio.h> 
#include <pthread.h>
#include <unistd.h> //threadStack.c
void * my_fun(void * param)
{
	printf("This is a thread that received %d\n", *(int *)param);
	return (void *)3;
}
int main(void)
{
	pthread_t t_id;
	int arg=10;
	pthread_create(&t_id, NULL, my_fun, (void *)&arg);
	arg=20; //Cambia il valore di arg e, poiché il puntatore alla variabile arg è stato passato come parametro alla funzione del thread, questa modifica influenzerà anche il valore ricevuto dalla funzione del thread
	sleep(3);
}

Nel kernel…

$ ./threadList.out & ps -eLf #Esegue il programma threadList.out in background mediante l'utilizzo di & (stampa sul terminale il PID del processo in esecuzione in background). Il comando ps -eLf mostra informazioni sui processi attualmente in esecuzione sul sistema: `-e` indica di mostrare informazioni su tutti i processi, l’opzione `-L` indica di mostrare informazioni sui thread e l’opzione `-f` indica di utilizzare un formato completo per l’output
#include <pthread.h> //threadList.c
void * my_fun(void * param)
{
	while(1);
}
	int main(void){
	pthread_t t_id;
	pthread_create(&t_id, NULL, my_fun, NULL);
	while(1);
}

Terminazione

Un nuovo thread termina in uno dei seguenti modi:

Cancellazione di un thread

int pthread_cancel(pthread_t thread);

Invia una richiesta di cancellazione al thread specificato, il quale reagirà (come e quando) a seconda di due suoi attributi: cancel state e cancel type.

Il cancel state di un thread definisce se il thread deve terminare o meno quando una richiesta di cancellazione viene ricevuta. Il cancel type di un thread definisce come il thread deve terminare.

Sia lo stato che il tipo possono essere definiti alla creazione del thread o durante la sua esecuzione, all’interno del thread stesso.

Cambiare il thread cancel state

int pthread_setcancelstate(int state, int *oldstate);

Modifica il cancel state del thread in esecuzione. Mentre oldstate viene riempito con lo stato precedente, state può contenere una delle seguenti macro:

Cambiare il thread cancel type

int pthread_setcanceltype(int type, int *oldtype);

Modifica il cancel type del thread in esecuzione. Mentre oldtype viene riempito con il type precedente, type può contenere una delle seguenti macro:

Cancellation points sono delle specifiche funzioni definite nella libreria pthread.h (list). Di solito sono system calls.

Esempio cancellazione 1

#include <stdio.h>
#include <pthread.h>
#include <unistd.h> //thCancel.c
int i = 1; //Variabile globale che serve per controllare se lo stato di cancellabilità del thread deve essere modificato
void * my_fun(void * param)
{
	if(i--)
	{
		pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL); //Dato che i vale 1 si entra nell'if e poi la si decrementa, poi viene modificato lo stato del primo thread creato, in modo che non sarà cancellabile
	}
	printf("Thread %ld started\n",*(pthread_t *)param); //Converte param in un puntatore a pthread_t per stampare l'ID
	sleep(3);
	printf("Thread %ld finished\n",*(pthread_t *)param);
}
int main(void)
{
	pthread_t t_id1, t_id2;
	pthread_create(&t_id1, NULL, my_fun, (void *)&t_id1); //Crea una thread
	sleep(1);
	pthread_cancel(t_id1); //Invia una richiesta di cancellazione al primo thread, ma dato che lo stato di cancellabilità del primo thread è stato impostato su PTHREAD_CANCEL_DISABLE, questa richiesta di cancellazione non avrà alcun effetto e il primo thread continuerà ad eseguire normalmente
	printf("Sent cancellation request for thread %ld\n",t_id1);
	pthread_create(&t_id2, NULL, my_fun, (void *)&t_id2); //Crea un'altra thread
	sleep(1);
	pthread_cancel(t_id2); //Invia una richiesta di cancellazione al secondo thread, ma dato che il valore di i ora e' 0, la funzione del thread non chiamerà la funzione pthread_setcancelstate e lo stato di cancellabilità del secondo thread rimarrà impostato sul valore predefinito di PTHREAD_CANCEL_ENABLE. Ciò significa che il secondo thread sarà cancellabile e risponderà alla richiesta di cancellazione
	printf("Sent cancellation request for thread %ld\n",t_id2);
	sleep(5); printf("Terminating program\n");
}

Esempio cancellazione 2

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h> //thCancel2.c
int tmp = 0;
void * my_fun(void * param)
{
	pthread_setcanceltype(*(int *)param,NULL); //Imposta il tipo di cancellazione del thread. Il tipo di cancellazione viene passato come primo argomento e ottenuto dalla funzione my_fun, convertendolo in un puntatore a int e dereferenziandolo
	for (long unsigned i = 0; i < (0x9FFF0000); i++); //Un ciclo che non fa nulla se non attendere
	tmp++;
	open("/tmp/tmp",O_RDONLY); //Apre il file in modalita' sola lettura. Questa chiamata è un punto di cancellazione, il che significa che se il thread è impostato per essere annullato in modo differito, verrà annullato a questo punto. Nel caso asincrono non fa nulla
	return NULL;
}
int main(int argc, char ** argv) //call program with ‘async’ or ‘defer’
{
	pthread_t t_id1; //Identificatore di thread
	int arg; //Per memorizzare il tipo di cancellazione del thread
	if(!strcmp(argv[1],"async")) //Indica il tipo di cancellazione del thread (asincrona), impostato nella linea di comando
	{
		arg = PTHREAD_CANCEL_ASYNCHRONOUS;
	}
	else if(!strcmp(argv[1],"defer")) //Indica il tipo di cancellazione del thread (asincrona)
	{
		arg = PTHREAD_CANCEL_DEFERRED;
	}
	pthread_create(&t_id1, NULL, my_fun, (void *)&arg); //Crea un nuovo thread. Il primo argomento alla funzione è l’indirizzo della variabile t_id1, che verrà utilizzato per memorizzare l’identificatore del thread creato. Il secondo argomento è NULL, il che significa che verranno utilizzati gli attributi predefiniti del thread. Il terzo argomento è la funzione del thread, ovvero la funzione my_fun. Il quarto argomento è un puntatore alla variabile arg, che verrà passato alla funzione del thread come parametro
	sleep(1);
	pthread_cancel(t_id1); //Annulla immediatamente il thread creato a cui era stata impostata la cancellazione asincrona
	sleep(5);
	printf("Tmp %d\n",tmp);
}
./thCancel2.c async #per la cancellazione asincrona
./thCancel2.c defer #per la cancellazione differita

Aspettare un thread: join

Un processo (thread) che avvia un nuovo thread può aspettare la sua terminazione mediante la funzione:

int pthread_join(pthread_t thread, void ** retval);

Questa funzione ritorna quando il thread identificato da thread termina, o subito se il thread è già terminato. Se il valore di ritorno del thread, ovvero il valore restituito dal thread quando termina, non è nullo (per esempio eseguendo la funzione pthread_exit() oppure dal valore di return quando termina normalmente), esso viene salvato nella variabile puntata da retval. Se il thread era stato cancellato, retval è riempito con PTHREAD_CANCELED.

Solo se il thread è joinable può essere aspettato! Un thread può essere aspettato da al massimo un thread!

Detach state di un thread

I thread sono creati per impostazione predefinita nello stato joinable, che consente a un altro thread di attendere la loro terminazione tramite il comando pthread_join(). I thread joinable non rilasciano le loro risorse alla terminazione, ma le mantengono fino a quando un altro thread non chiama pthread_join() su di esso. Questo consente al thread principale di recuperare il valore di ritorno del thread e lo stato di uscita. Se pthread_join() non viene mai chiamato su un thread joinable, le sue risorse rimarranno allocate fino alla terminazione del processo. Al contrario, i thread detached rilasciano le loro risorse immediatamente al termine, ma non permettono ad altri processi di aspettarli.

NB: un thread detached non può diventare joinable durante la sua esecuzione, mentre è possibile il contrario.

Cambiare il detach state

int pthread_detach(pthread_t thread); //Si passa il puntatore all'ID del thread

Questo comando può essere eseguito da un thread qualunque e cambia il detach state di thread da joinable a detached.
Ricorda che una volta cambiato lo stato non si può invertire.

Esempio join 1

#include <stdio.h>
#include <pthread.h>
#include <unistd.h> //thJoin.c
void * my_fun(void * param)
{
	printf("Thread %ld started\n",*(pthread_t *)param); 
	sleep(3);
	char * str = "Returned string";
	pthread_exit((void *)str); //Il thread termina restituendo la stringa in str. Si puo' utilizzare anche 'return (void *) str;'
}
int main(void)
{
	pthread_t t_id;
	void * retFromThread; //This must be a pointer to void!
	pthread_create(&t_id, NULL, my_fun, (void *)&t_id); //Crea un nuovo thread ed esgue my_fun, passando l’indirizzo del suo identificatore come parametro
	pthread_join(t_id,&retFromThread); //Il thread principale attende la terminazione del nuovo thread.
	//Quando il nuovo thread termina, il suo valore di ritorno viene salvato nella variabile retFromThread
	printf("Thread %ld returned '%s'\n",t_id,(char *)retFromThread); //Viene fatto un cast e quindi si converte il valore di ritorno in un puntatore a char
}

Esempio join 2

#include <stdio.h>
#include <pthread.h>
#include <unistd.h> //threadJoin2.c
void * my_fun(void *param)
{
	printf("This is a thread that received %d\n", *(int *)param); //Converte il puntatore a void ricevuto come parametro in un puntatore a int e ne recupera il valore, stampando un messaggio
	return (void *)3; //Il thread termina restituendo il valore intero 3
}
int main(void)
{
	pthread_t t_id;
	int arg=10, retval;
	pthread_create(&t_id, NULL, my_fun, (void *)&arg); //Crea un nuovo thread ed esgue my_fun, passando l’indirizzo della variabile intera arg come parametro
	printf("Executed thread with id %ld\n",t_id); //Il thread principale stampa un messaggio per indicare che ha creato il nuovo thread
	sleep(3);
	pthread_join(t_id, (void **)&retval); //Attende la terminazione del nuovo thread e poi recupera il suo valore di ritorno
	printf("retval=%d\n", retval);
}

Esempio join 3

#include <stdio.h>
#include <pthread.h>
#include <unistd.h> //threadJoin2.c
void * my_fun(void *param)
{
	printf("This is a thread that received %d\n", *(int *)param); //Converte il puntatore a void ricevuto come parametro in un puntatore a int e ne recupera il valore, stampando un messaggio
	int i = *(int *)param * 2; //Crea una variabile a cui viene assegnato un valore ottenuto moltiplicando per 2 il valore intero ricevuto come parametro dal thread. Ma dato che la variabile i e' una variabile locale alla funzione, cessa di esistere quando la funzione termina
	return (void *)&i; //Questa riga restituisce l’indirizzo di questa variabile locale come valore di ritorno del thread. Il comportamento e' pericoloso perché una volta che la funzione my_fun() termina e la variabile i cessa di esistere, l’indirizzo restituito dal thread non è più valido. Se il thread principale tenta di accedere a questo indirizzo per recuperare il valore di ritorno del thread il comportamento del programma diventa indefinito e potrebbe causare errori o crash
}
int main(void)
{
	pthread_t t_id;
	int arg=10, retval;
	pthread_create(&t_id, NULL, my_fun, (void *)&arg); //Crea un nuovo thread ed esgue my_fun, passando l’indirizzo della variabile intera arg come parametro
	printf("Executed thread with id %ld\n",t_id); //Il thread principale stampa un messaggio per indicare che ha creato il nuovo thread
	sleep(3);
	pthread_join(t_id, (void **)&retval); //Attende la terminazione del nuovo thread e poi recupera il suo valore di ritorno
	printf("retval=%d\n", retval);
}

Attributi di un thread

La struttura pthread_attr_t viene utilizzata per specificare gli attributi di un thread quando viene creato utilizzando la funzione pthread_create(). Questa struttura contiene informazioni come la dimensione dello stack del thread, lo stato del thread (joinable o detached), la priorità del thread e altri attributi. Quando si crea un nuovo thread, è possibile passare un puntatore a una struttura pthread_attr_t come secondo parametro della funzione pthread_create(). In questo modo, il nuovo thread viene creato con gli attributi specificati nella struttura. Tuttavia, una volta che il thread è stato creato, la struttura pthread_attr_t non è più utilizzata e non ha alcun effetto sul thread. Ciò significa che se si modifica la struttura pthread_attr_t dopo aver creato il thread, gli attributi del thread non cambieranno. Questo accade analogamente alla struttura utilizzata per gestire le maschere di segnale.

La struttura deve essere inizializzata, il che imposta tutti gli attributi al loro valore predefinito. Una volta utilizzata e non più necessaria, la struct deve essere distrutta.

I vari attributi della struttura possono e devono essere modificati individualmente con alcune funzioni dedicate.

Attributi di un thread 2

Inizializza la struttura con tutti gli attributi default.

int pthread_attr_init(pthread_attr_t *attr)

Distrugge la struttura.

int pthread_attr_destroy(pthread_attr_t *attr)

Imposta un certo attributo ad un certo valore.

int pthread_attr_setxxxx(pthread_attr_t *attr, params);

Ottiene un certo attributo

int pthread_attr_getxxxx(const pthread_attr_t *attr, params);

xxxx Attributi di un thread

Esempio attributi

#include <stdio.h>
#include <pthread.h>
#include <unistd.h> //threadAttr.c
void * my_fun(void *param)
{
	printf("This is a thread that received %d\n", *(int *)param);
	return (void*)3;
}
int main(void)
{
	pthread_t t_id; //Per memorizzare l'ID del thread
	pthread_attr_t attr; //Memorizza gli attributi del thread
	int arg=10;
	int detachSTate;
	pthread_attr_init(&attr); //Inizializza gli attributi del thread
	pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); //Set detached
	pthread_attr_getdetachstate(&attr,&detachSTate); //Ottiene lo stato di detached e lo memorizza in detachSTate.
	if(detachSTate == PTHREAD_CREATE_DETACHED) //Se lo stato e' detached allora esegue l'if
	{
		printf("Detached\n");
	}
	pthread_create(&t_id, &attr, my_fun, (void *)&arg); //Viene creato il thread
	printf("Executed thread with id %ld\n",t_id);
	pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_JOINABLE); //Si tenta di impostare lo stato di joinable, ma questo non ha effetto perche' gli attributi del thread vengono utilizzati solo al momento della creazione del thread
	sleep(3); 
	pthread_attr_destroy(&attr); //Distrugge gli attributi del thread
	int esito = pthread_join(t_id, (void **)&detachSTate); //Fallisce perche' il thread è stato creato in modalità “detached” (non si puo' aspettare un thread se ha questo stato)
	printf("Esito '%d' is different 0\n", esito);
}

Threads e segnali

Quando viene inviato un segnale ad un processo non si può sapere quale thread andrà a gestirlo. Per evitare problemi o comportamenti inattesi, è importante gestire correttamente le maschere dei segnali dei singoli thread con pthread_attr_setsigmask_np(pthread_attr_t *attr, const sigset_t *sigmask) la quale usa *sigmask per impostare la maschera dei segnali nella struttura *attr. Questo permette di specificare quali segnali devono essere bloccati o ignorati dai thread creati con gli attributi specificati.

È poi possibile usare funzioni come sigwait() e sigwaitinfo() per gestire l’attesa di un segnale. Infine, è possibile inviare un segnale ad un thread specifico usando int pthread_kill(pthread_t thread, int sig);

Mutex

Il problema della sincronizzazione

Quando eseguiamo un programma con più thread essi condividono alcune risorse, tra le quali le variabili globali. Se entrambi i thread accedono ad una sezione di codice condivisa ed hanno la necessità di accedervi in maniera esclusiva allora dobbiamo instaurare una sincronizzazione. I risultati, altrimenti, potrebbero essere inaspettati.

Esempio

#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h> //syncProblem.c
pthread_t tid[2]; //Array di due elementi per memorizzare gli ID dei thread
int counter = 0;
void *thr1(void *arg) //Funzione eseguita dal primo thread
{
	counter = 1;
	printf("Thread 1 has started with counter %d\n",counter); //Stampa del messaggio "Thread 1 has started with counter 1"
	for (long unsigned i = 0; i < (0x00FF0000); i++); //Ciclo for vuoto per attendere alcuni cicli
	counter += 1;
	printf("Thread 1 expects 2 and has: %d\n", counter); //Stampa del messaggio "Thread 1 expects 2 and has: %d" dove %d è il valore attuale di "counter"
	return NULL;
}
void *thr2(void *arg) //Funzione eseguita dal secondo thread
{
	counter = 10;
	printf("Thread 2 has started with counter %d\n",counter); //Stampa del messaggio "Thread 2 has started with counter 10"
	for (long unsigned i = 0; i < (0xFFF0000); i++); //Ciclo for vuoto per attendere alcuni cicli
	counter += 1;
	printf("Thread 2 expects 11 and has: %d\n", counter); //Stampa del messaggio "Thread 2 expects 11 and has: %d" dove %d è il valore attuale di "counter"
	return NULL;
}
int main(void)
{
	pthread_create(&(tid[0]), NULL, thr1,NULL); //Creazione del primo thread che eseguirà la funzione "thr1"
	pthread_create(&(tid[1]), NULL, thr2,NULL); //Creazione del secondo thread che eseguirà la funzione "thr2"
	pthread_join(tid[0], NULL);
	pthread_join(tid[1], NULL);
	return 0;
}

Dato che il programma non utilizza alcun meccanismo di sincronizzazione per garantire l’accesso esclusivo alla variabile counter, l’ordine in cui i thread accedono e modificano il valore di counter non è deterministico. Di conseguenza, il risultato finale potrebbe non essere quello previsto.

Una possibile soluzione: mutex

I mutex sono dei semafori imposti ai thread. Essi possono proteggere una determinata sezione di codice, consentendo ad un thread di accedervi in maniera esclusiva fino allo sblocco del semaforo. Ogni thread che vorrà accedere alla stessa sezione di codice dovrà aspettare che il semaforo sia sbloccato, andando in sleep fino alla sua prossima schedulazione.

I mutex vanno inizializzati e poi assegnati ad una determinata sezione di codice. Il blocco e sblocco è manuale.

NB: i mutex non regolano l’accesso alla memoria o alle variabile, ma solo a porzioni di codice.

Creare e distruggere un mutex

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr)

int pthread_mutex_destroy(pthread_mutex_t *mutex)

#include <pthread.h> //createMutex.c
pthread_mutex_t lock; //Variabile globale che rappresenta il mutex
int main(void)
{
	pthread_mutex_init(&lock, NULL); //Viene creato il mutex con gli attributi predefiniti
	pthread_mutex_destroy(&lock); //Viene distrutto il mutex, ma puo' essere reinizializzato utilizzando nuovamente la funzione di init
}

Bloccaggio e sbloccaggio di un mutex

int pthread_mutex_lock(pthread_mutex_t *mutex)

int pthread_mutex_unlock(pthread_mutex_t *mutex)

Dopo essere stato creato, un mutex deve essere bloccato per essere efficace. Non appena un thread lo blocca, un altro thread deve attendere che venga sbloccato prima di procedere al suo blocco.

Quando si richiama il blocco, un thread attende che il mutex sia libero e poi lo blocca.

#include <pthread.h>
pthread_mutex_t lock;
void * thread(void *)
{
	pthread_mutex_lock(&lock);
	/* Protected section of code */
	pthread_mutex_unlock(&lock);
}

Example

#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h> //mutex.c
pthread_mutex_t lock; //Variabile globale che rappresenta il mutex
pthread_t tid[2]; //Array di due elementi per memorizzare gli ID dei thread
int counter = 0;
void* thr1(void* arg)
{
	pthread_mutex_lock(&lock); //Ascquisisce il mutex bloccandolo
	counter = 1;
	printf("Thread 1 has started with counter %d\n",counter);
	for (long unsigned i = 0; i < (0x00FF0000); i++); //Ciclo for vuoto per attendere alcuni cicli
	counter += 1;
	pthread_mutex_unlock(&lock); //Rilascia il mutex
	printf("Thread 1 expects 2 and has: %d\n", counter);
	return NULL;
}
void * thr2(void* arg)
{
	pthread_mutex_lock(&lock);
	counter = 10;
	printf("Thread 2 has started with counter %d\n",counter);
	for (long unsigned i = 0; i < (0xFFF0000); i++); //Ciclo for vuoto per attendere alcuni cicli
	counter += 1;
	pthread_mutex_unlock(&lock);
	printf("Thread 2 expects 11 and has: %d\n", counter);
	return NULL;
}
int main(void)
{
	pthread_mutex_init(&lock, NULL); //Viene creato il mutex con gli attributi predefiniti
	pthread_create(&(tid[0]), NULL, thr1,NULL); //Viene creato il primo thread che esegue la funzione thr1
	pthread_create(&(tid[1]), NULL, thr2,NULL); //Viene creato il secondo thread che esegue la funzione thr2
	pthread_join(tid[0], NULL); //Attende il completamento del thread 1
	pthread_join(tid[1], NULL); //Attende il completamento del thread 2
	pthread_mutex_destroy(&lock); //Distrugge il mutex
}

Tipi di mutex

Blocca quando già bloccato Sblocca quando bloccato da altri Sblocca se già sbloccato
PTHREAD_MUTEX_NORMAL deadlock undefined undefined
PTHREAD_MUTEX_ERRORCHECK error error error
PTHREAD_MUTEX_RECURSIVEK Lock count++ so that it requires the same number of unlocks error error
PTHREAD_MUTEX_DEFAULT undefined undefined undefined

Example

#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h> //recursive.c
pthread_mutex_t lock; //Variabile che rappresenta un mutex
pthread_t tid[2]; //Array di due elementi per memorizzare gli ID dei thread
int counter = 0;
void* thr1(void* arg)
{
	pthread_mutex_lock(&lock); pthread_mutex_lock(&lock); //Il thread 1 acquisisce il mutex due volte, non si blocca perche' e' ricorsivo
	counter = 1;
	printf("Thread 1 has started with counter %d\n",counter);
	for (long unsigned i = 0; i < (0x00FF0000); i++); //Ciclo for vuoto per attendere alcuni cicli
	counter += 1;
	pthread_mutex_unlock(&lock); //Rilascia una sola volta il mutex
	printf("Thread 1 expects 2 and has: %d\n", counter);
	pthread_mutex_unlock(&lock); //Rilascia il mutex una seconda volta in modo che anche altri thread possano acquisirlo
	return NULL;
}
void* thr2(void* arg)
{
	pthread_mutex_lock(&lock); pthread_mutex_lock(&lock); //Il thread 2 acquisisce il mutex due volte, non si blocca perche' e' ricorsivo
	counter = 10;
	printf("Thread 2 has started with counter %d\n",counter);
	for (long unsigned i = 0; i < (0xFFF0000); i++); //Ciclo for vuoto per attendere alcuni cicli
	counter += 1;
	pthread_mutex_unlock(&lock); //Rilascia una sola volta il mutex
	printf("Thread 2 expects 11 and has: %d\n", counter);
	return NULL;
}
int main()
{
	pthread_mutexattr_t attr; //Variabile che specifica gli attributi del mutex
	pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE); //Imposta gli attributi del mutex come ricorsivi. In questo caso un mutex ricorsivo consente a un thread di bloccare più volte lo stesso mutex senza causare un deadlock. Questo tipo di mutex deve essere sbloccato lo stesso numero di volte in cui è stato bloccato prima che il mutex venga restituito in uno stato sbloccato
	pthread_mutex_init(&lock, &attr); //Inizializza il mutex con gli attributi specificati
	pthread_create(&(tid[0]), NULL, thr1,NULL); //Viene creato il primo thread che esegue la funzione thr1
	pthread_create(&(tid[1]), NULL, thr2,NULL); //Viene creato il secondo thread che esegue la funzione thr2
	pthread_join(tid[0], NULL); //Attende il completamento del thread 1
	pthread_join(tid[1], NULL); //Attende il completamento del thread 2
	pthread_mutex_destroy(&lock); //Distrugge il mutex
	return 0;
}

Conclusioni

I “thread” sono una sorta di “processi leggeri” che permettono di eseguire funzioni “in concorrenza” in modo più semplice rispetto alla generazioni di processi veri e propri (forking).

I “MUTEX” sono un metodo semplice ma efficace per eseguire sezioni critiche in processi multithread.

È importante limitare al massimo la sezione critica utilizzando lock/unlock per la porzione di codice più piccola possibile, e solo se assolutamente necessario (per esempio quando possono capitare accessi concorrenti).