🚀 Nuova versione beta disponibile! Feedback o problemi? Contattaci

Sincronizzazione con Mutex e Semafori in C

Codegrind Team•Aug 23 2024

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.