Sincronizzazione con Mutex e Semafori in C
La sincronizzazione è un concetto chiave nella programmazione multithread, che assicura che le risorse condivise vengano utilizzate in modo sicuro e coerente dai vari thread. In C, gli strumenti principali per gestire la sincronizzazione includono mutex e semafori. Questa guida esplora come utilizzare mutex e semafori per sincronizzare i thread, prevenire race conditions e garantire che le risorse condivise vengano accessibili in modo sicuro.
Mutex
Un mutex (mutual exclusion) è un meccanismo che permette a un solo thread di accedere a una risorsa condivisa alla volta. Quando un thread blocca un mutex, altri thread che tentano di bloccarlo vengono messi in attesa fino a quando il mutex non viene sbloccato.
Creazione e Utilizzo di un Mutex
Per utilizzare un mutex, è necessario dichiarare una variabile di tipo pthread_mutex_t
e inizializzarla. Successivamente, si utilizzano le funzioni pthread_mutex_lock
e pthread_mutex_unlock
per bloccare e sbloccare il mutex.
Esempio di Uso di un Mutex
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mio_mutex;
int contatore = 0;
void *incrementa_contatore(void *arg) {
pthread_mutex_lock(&mio_mutex);
contatore++;
printf("Contatore: %d\n", contatore);
pthread_mutex_unlock(&mio_mutex);
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_mutex_init(&mio_mutex, NULL);
pthread_create(&thread1, NULL, incrementa_contatore, NULL);
pthread_create(&thread2, NULL, incrementa_contatore, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_mutex_destroy(&mio_mutex);
return 0;
}
In questo esempio, il mutex mio_mutex
garantisce che solo un thread alla volta possa incrementare il valore del contatore
e stamparlo, evitando una race condition.
Inizializzazione e Distruzione di un Mutex
pthread_mutex_init
: Inizializza un mutex.pthread_mutex_destroy
: Distrugge un mutex quando non è più necessario.
Mutex Ricorsivi
Un mutex ricorsivo permette al thread che lo ha bloccato di bloccarlo nuovamente senza causare un deadlock. È utile quando un thread deve bloccare lo stesso mutex più volte, ad esempio in una funzione ricorsiva.
Esempio di Mutex Ricorsivo
pthread_mutex_t mio_mutex;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mio_mutex, &attr);
pthread_mutexattr_destroy(&attr);
Semafori
Un semaforo è un meccanismo di sincronizzazione che utilizza un contatore per controllare l’accesso a una risorsa condivisa. I semafori possono essere binari (che funzionano come un mutex) o contatori, che permettono a più thread di accedere alla risorsa fino a un limite definito.
Creazione e Utilizzo di un Semaforo
Per utilizzare un semaforo, si utilizza la libreria POSIX con semaphore.h
. È necessario dichiarare una variabile di tipo sem_t
e inizializzarla.
Esempio di Uso di un Semaforo
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
sem_t semaforo;
int contatore = 0;
void *incrementa_contatore(void *arg) {
sem_wait(&semaforo); // Decrementa il semaforo e blocca se il contatore è 0
contatore++;
printf("Contatore: %d\n", contatore);
sem_post(&semaforo); // Incrementa il semaforo e sblocca altri thread se necessario
return NULL;
}
int main() {
pthread_t thread1, thread2;
sem_init(&semaforo, 0, 1); // Inizializza il semaforo con valore 1
pthread_create(&thread1, NULL, incrementa_contatore, NULL);
pthread_create(&thread2, NULL, incrementa_contatore, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
sem_destroy(&semaforo); // Distrugge il semaforo
return 0;
}
In questo esempio, il semaforo semaforo
garantisce che solo un thread alla volta possa incrementare e stampare il valore di contatore
.
Inizializzazione e Distruzione di un Semaforo
sem_init
: Inizializza un semaforo.sem_destroy
: Distrugge un semaforo quando non è più necessario.
Semafori Contatori
I semafori contatori permettono a un numero limitato di thread di accedere a una risorsa. Il valore iniziale del semaforo determina quanti thread possono accedere simultaneamente.
Esempio di Semaforo Contatore
sem_init(&semaforo, 0, 3); // Fino a 3 thread possono accedere contemporaneamente
Confronto tra Mutex e Semafori
Mutex
- Uso Tipico: Sincronizzazione di thread che accedono a una singola risorsa condivisa.
- Proprietà : Un mutex può essere bloccato e sbloccato solo dal thread che lo ha bloccato.
- Efficienza: Generalmente più efficiente per la sincronizzazione semplice.
Semafori
- Uso Tipico: Controllo dell’accesso a risorse condivise da più thread, specialmente quando più di un thread può accedere contemporaneamente.
- Proprietà : Un semaforo può essere modificato da qualsiasi thread.
- Flessibilità : Più flessibile per scenari complessi, come la gestione di pool di risorse.
Evitare Problemi di Sincronizzazione
1. Deadlock
Un deadlock si verifica quando due o più thread rimangono bloccati in attesa l’uno dell’altro, creando un ciclo di dipendenze che non può essere risolto.
Prevenzione del Deadlock
- Ordine Consistente: Bloccare i mutex sempre nello stesso ordine in tutti i thread.
- Timeout: Utilizzare variabili di condizione con timeout per evitare attese infinite.
2. Race Condition
Una race condition si verifica quando il comportamento del programma dipende dall’ordine in cui i thread accedono alle risorse condivise.
Prevenzione della Race Condition
- Uso di Mutex: Bloccare l’accesso a risorse condivise con mutex.
- Uso di Semafori: Limitare l’accesso simultaneo alle risorse con semafori contatori.
Conclusioni
La sincronizzazione con mutex e semafori è essenziale per la programmazione multithread sicura ed efficiente in C. Comprendere quando e come utilizzare questi strumenti ti permette di prevenire problemi comuni come deadlock e race conditions, garantendo che i tuoi programmi funzionino correttamente anche in ambienti di esecuzione parallela. Con una buona padronanza dei concetti di sincronizzazione, potrai sviluppare applicazioni multithread robuste e scalabili.