Variabili di condizione e produttore-consumatore
Usa le variabili di condizione per coordinare i thread in modo efficiente. Costruisci una coda produttore-consumatore thread-safe, gestisci i risvegli spuri ed evita il problema del risveglio perso.
Coordinare i thread con le variabili di condizione
Una variabile di condizione permette a un thread di attendere finché una certa condizione non diventa vera, senza cicli di attesa attiva. Invece di girare a vuoto con while (!ready) { /* burn CPU */ }, il thread si mette in pausa e viene risvegliato da un altro thread che chiama notify_one() o notify_all().
std::condition_variable funziona solo con std::unique_lock. Il pattern è sempre questo:
1. Acquisisci il lock sul mutex.
2. Controlla la condizione — se è falsa, chiama wait(), che rilascia atomicamente il mutex e sospende il thread.
3. Quando arriva la notifica (e la condizione è vera), il mutex viene riacquisito e l'esecuzione prosegue.
Il thread che notifica modifica lo stato condiviso sotto lo stesso mutex, poi chiama notify_one() o notify_all().
Uso di base di una variabile di condizione
Ecco il pattern fondamentale: un thread attende i dati, un altro glieli fornisce.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <string>
std::mutex mtx;
std::condition_variable cv;
std::string data;
bool ready = false;
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
// Wait until ready is true (handles spurious wakeups)
cv.wait(lock, []{ return ready; });
std::cout << "Consumer received: " << data << "\n";
}
void producer() {
{
std::lock_guard<std::mutex> lock(mtx);
data = "Hello from producer";
ready = true;
} // unlock before notify to avoid waking thread into contention
cv.notify_one();
}
int main() {
std::thread t1(consumer);
std::thread t2(producer);
t1.join();
t2.join();
return 0;
}Una variabile di condizione può risvegliare un thread anche quando non è stata inviata alcuna notifica — questo fenomeno si chiama risveglio spurio. Lo standard C++ lo consente (ed è comune sui sistemi reali) per ragioni di prestazioni. Devi sempre usare la forma di wait() con predicato oppure racchiuderla in un ciclo while:
// CORRETTO — la forma con predicato gestisce i risvegli spuri
cv.wait(lock, []{ return ready; });
// Ciclo manuale EQUIVALENTE
while (!ready) {
cv.wait(lock);
}
// SBAGLIATO — si risveglia a caso e prosegue senza dati
cv.wait(lock); // nessun controllo della condizione!Non chiamare mai cv.wait(lock) senza un predicato o senza un ciclo while che lo racchiuda.
Buffer limitato thread-safe (produttore-consumatore)
Il pattern produttore-consumatore è uno dei pattern di concorrenza più diffusi. I produttori aggiungono elementi a un buffer condiviso; i consumatori li rimuovono. Un buffer limitato blocca i produttori quando è pieno e i consumatori quando è vuoto.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
template <typename T>
class BoundedQueue {
std::queue<T> queue_;
std::mutex mtx_;
std::condition_variable not_full_;
std::condition_variable not_empty_;
size_t capacity_;
public:
explicit BoundedQueue(size_t cap) : capacity_(cap) {}
void push(T item) {
std::unique_lock<std::mutex> lock(mtx_);
not_full_.wait(lock, [this]{ return queue_.size() < capacity_; });
queue_.push(std::move(item));
not_empty_.notify_one();
}
T pop() {
std::unique_lock<std::mutex> lock(mtx_);
not_empty_.wait(lock, [this]{ return !queue_.empty(); });
T item = std::move(queue_.front());
queue_.pop();
not_full_.notify_one();
return item;
}
};
int main() {
BoundedQueue<int> bq(5); // capacity 5
// Producer
std::thread producer([&bq]() {
for (int i = 0; i < 20; ++i) {
bq.push(i);
std::cout << "Produced: " << i << "\n";
}
});
// Consumer
std::thread consumer([&bq]() {
for (int i = 0; i < 20; ++i) {
int val = bq.pop();
std::cout << "Consumed: " << val << "\n";
}
});
producer.join();
consumer.join();
return 0;
}Il problema del risveglio perso
Un risveglio perso si verifica quando notify_one() viene chiamata prima che il consumatore chiami wait(). Dato che nessun thread è in attesa, la notifica va persa — e il consumatore dorme per sempre.
Il pattern standard previene questa situazione controllando il predicato prima di mettersi in attesa. Se il produttore ha già portato la condizione a vera, il predicato restituisce true e wait() ritorna immediatamente senza sospendere il thread. Ecco perché la forma con predicato cv.wait(lock, pred) è essenziale — controlla la condizione prima e dopo ogni risveglio:
1. Acquisisci il lock sul mutex.
2. Controlla il predicato — se è già vero, salta l'attesa.
3. Altrimenti, rilascia atomicamente il lock e mettiti in attesa.
4. Al risveglio, riacquisisci il lock e ricontrolla il predicato.
Finché sia il produttore sia il consumatore proteggono il flag condiviso con lo stesso mutex, nessun risveglio può andare perso.
notify_one vs notify_all
notify_one() risveglia un solo thread in attesa; notify_all() li risveglia tutti. Usa notify_all() quando più thread potrebbero dover ricontrollare le proprie condizioni (per esempio i segnali di arresto) e notify_one() quando deve proseguire esattamente un thread (per esempio un singolo consumatore che preleva un task).
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <atomic>
std::mutex mtx;
std::condition_variable cv;
bool shutdown = false;
void worker(int id) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return shutdown; });
std::cout << "Worker " << id << " shutting down\n";
}
int main() {
std::vector<std::thread> workers;
for (int i = 0; i < 5; ++i) {
workers.emplace_back(worker, i);
}
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
shutdown = true;
}
cv.notify_all(); // wake ALL workers for shutdown
for (auto& w : workers) w.join();
return 0;
}Usa sempre l'overload di wait() con predicato per proteggerti dai risvegli spuri e da quelli persi. Quando puoi, rilascia il mutex prima di chiamare notify_one()/notify_all() — così eviti che il thread appena risvegliato si blocchi subito sul mutex. Preferisci notify_one() quando deve risvegliarsi un solo thread e notify_all() per gli scenari di broadcast come l'arresto. Non attendere mai su una variabile di condizione con un lock_guard — ti serve unique_lock.
std::condition_variablepermette ai thread di dormire finché una condizione non è vera, evitando la costosa attesa attiva- Usa sempre la forma di
wait()con predicato per gestire sia i risvegli spuri sia quelli persi - Il pattern produttore-consumatore usa due variabili di condizione: "non pieno" e "non vuoto"
notify_one()risveglia un solo thread in attesa;notify_all()li risveglia tutti — scegli quella giusta per il tuo scenario- Sia chi attende sia chi notifica devono usare lo stesso mutex per proteggere la condizione condivisa
Quiz — Metti alla prova le tue conoscenze
(20 XP)1. Perché devi usare sempre un predicato con `condition_variable::wait()`?
2. In una coda produttore-consumatore limitata, cosa dovrebbe fare il produttore quando la coda è piena?
3. Quale tipo di lock richiede `std::condition_variable::wait()`?