Mutex e Semafori in C++
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 alock_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 immediatamentefalse
.
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.