🚀 Nuova versione beta disponibile! Feedback o problemi? Contattaci

Mutex e Semafori in C++

Codegrind TeamAug 23 2024

La programmazione concorrente in C++ permette l’esecuzione simultanea di più thread, sfruttando al meglio le risorse di un sistema multicore. Tuttavia, questa concorrenza introduce complessità legate alla sincronizzazione tra i thread, necessaria per evitare problemi come le condizioni di gara (race conditions) e garantire un accesso sicuro alle risorse condivise. I mutex e i semafori sono due strumenti fondamentali forniti da C++ per gestire questa sincronizzazione. In questo articolo, esploreremo come funzionano mutex e semafori in C++, come utilizzarli correttamente e le best practices per prevenire errori comuni nella programmazione concorrente.

Mutex in C++

Cos’è un Mutex?

Un mutex (abbreviazione di “mutual exclusion”) è un meccanismo di sincronizzazione che permette di limitare l’accesso a una risorsa condivisa a un solo thread alla volta. Quando un thread blocca un mutex, altri thread che tentano di bloccare lo stesso mutex vengono messi in attesa fino a quando il mutex non viene sbloccato.

Utilizzo di un Mutex

In C++, i mutex sono forniti dalla libreria <mutex> e vengono utilizzati per proteggere sezioni critiche del codice che non possono essere eseguite contemporaneamente da più thread.

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;  // Dichiarazione di un mutex

void stampaNumero(int n) {
    std::lock_guard<std::mutex> lock(mtx);  // Blocca il mutex automaticamente
    std::cout << "Thread #" << n << std::endl;
    // Il mutex viene automaticamente sbloccato alla fine del blocco
}

int main() {
    std::thread t1(stampaNumero, 1);
    std::thread t2(stampaNumero, 2);

    t1.join();
    t2.join();

    return 0;
}

In questo esempio, il std::lock_guard viene utilizzato per bloccare il mutex mtx all’inizio della funzione stampaNumero e sbloccarlo automaticamente quando l’oggetto lock esce dallo scope.

Alternative al lock_guard

Oltre a std::lock_guard, C++ fornisce altri strumenti per la gestione dei mutex:

  • std::unique_lock: Offre maggiore flessibilità rispetto a lock_guard, permettendo di bloccare e sbloccare manualmente il mutex.
std::unique_lock<std::mutex> lock(mtx);
lock.unlock();  // Sblocca il mutex manualmente
lock.lock();    // Blocca di nuovo il mutex
  • std::try_lock: Tenta di bloccare un mutex senza bloccare il thread chiamante. Se il mutex è già bloccato, std::try_lock restituisce immediatamente false.
if (mtx.try_lock()) {
    // Se il mutex è disponibile, esegui il codice
    mtx.unlock();
} else {
    // Altrimenti, fai qualcosa d'altro
}

Condizioni di Gara e Mutex

Le condizioni di gara si verificano quando più thread accedono e modificano simultaneamente una risorsa condivisa senza una corretta sincronizzazione, causando comportamenti imprevisti. Utilizzare mutex è un modo per prevenire tali condizioni, garantendo che solo un thread alla volta possa eseguire sezioni critiche del codice.

#include <iostream>
#include <thread>
#include <mutex>

int contatore = 0;
std::mutex mtx;

void incrementaContatore() {
    for (int i = 0; i < 1000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++contatore;
    }
}

int main() {
    std::thread t1(incrementaContatore);
    std::thread t2(incrementaContatore);

    t1.join();
    t2.join();

    std::cout << "Valore finale del contatore: " << contatore << std::endl;
    return 0;
}

In questo esempio, il mutex mtx assicura che il contatore venga incrementato correttamente, evitando una condizione di gara.

Semafori in C++

Cos’è un Semaforo?

Un semaforo è un meccanismo di sincronizzazione più avanzato rispetto a un mutex, che può essere utilizzato per controllare l’accesso a una risorsa da parte di più thread. Un semaforo utilizza un contatore per tenere traccia del numero di permessi disponibili per accedere alla risorsa. Quando un thread tenta di accedere alla risorsa, il semaforo decrementa il contatore. Se il contatore è maggiore di zero, il thread può procedere; altrimenti, il thread viene bloccato finché il contatore non diventa positivo.

Implementazione di Semafori in C++

C++ non include semafori nella libreria standard fino a C++20, ma è possibile implementare un semaforo utilizzando std::condition_variable e std::mutex.

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

class Semaforo {
public:
    Semaforo(int count = 0) : count(count) {}

    void signal() {
        std::unique_lock<std::mutex> lock(mtx);
        ++count;
        cv.notify_one();
    }

    void wait() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this]() { return count > 0; });
        --count;
    }

private:
    std::mutex mtx;
    std::condition_variable cv;
    int count;
};

int main() {
    Semaforo sem(1);

    auto funzione = [&sem](int id) {
        sem.wait();
        std::cout << "Thread " << id << " in esecuzione\n";
        std::this_thread::sleep_for(std::chrono::seconds(1));
        sem.signal();
    };

    std::thread t1(funzione, 1);
    std::thread t2(funzione, 2);

    t1.join();
    t2.join();

    return 0;
}

In questo esempio, il semaforo sem permette a un solo thread alla volta di eseguire la sezione critica del codice.

Differenze tra Mutex e Semaforo

Caratteristica Mutex Semaforo
Uso Protezione di sezioni critiche Controllo dell’accesso a risorse multiple
Contatore Non presente Presente
Thread singolo Permette l’accesso a un solo thread alla volta Permette l’accesso a più thread, limitato dal contatore
Semplicità Più semplice da utilizzare Più complesso, offre maggiore flessibilità

Best Practices

  • Usare mutex solo dove necessario: Bloccare sezioni di codice troppo grandi può causare colli di bottiglia e ridurre le prestazioni.
  • Evitare il deadlock: Assicurarsi che tutti i mutex siano sbloccati correttamente e considerare l’uso di std::unique_lock per maggiore controllo.
  • Utilizzare semafori per risorse limitate: Quando più thread devono accedere a un numero limitato di risorse, i semafori sono spesso la scelta migliore.
  • Evitare la dipendenza circolare: Se più mutex sono necessari, assicurarsi di acquisirli sempre nello stesso ordine per prevenire deadlock.

Conclusione

Mutex e semafori sono strumenti potenti per gestire la concorrenza e la sincronizzazione in C++. Utilizzarli correttamente è essenziale per scrivere applicazioni concorrenti sicure ed efficienti. Conoscere le differenze tra questi meccanismi e comprendere quando e come utilizzarli permette di prevenire problemi comuni come condizioni di gara, deadlock e inefficienze, garantendo che il software sia robusto e performante.