Variabile de condiție și producător-consumator
Use condition variables to efficiently coordinate threads. Build a thread-safe producer-consumer queue, handle spurious wakeups, and avoid the lost wakeup problem.
Coordonarea firelor cu variabilele de condiție
A condition variable lets a thread wait until some condition becomes true, without busy-looping. Instead of spinningwhile (!ready) { /* burn CPU */ }, the thread sleeps and is woken up by another thread that callsnotify_one()ornotify_all().
std::condition_variableworks only withstd::unique_lock. The pattern is always:
1. Lock the mutex.
2. Check the condition — if false, callwait()which atomically releases the mutex and suspends the thread.
3. When notified (and the condition is true), the mutex is re-acquired and execution continues.
The notifying thread modifies the shared state under the same mutex, then callsnotify_one()ornotify_all().
Condiție de bază Utilizare variabilă
Here is the fundamental pattern: one thread waits for data, another provides it.
#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;
}O variabilă de condiție poate activa un fir de execuție chiar și atunci când nu a fost trimisă nicio notificare - aceasta se numește trezire falsă. Este permis de standardul C++ (și comun pe sistemele reale) din motive de performanță. Trebuie întotdeauna să utilizați forma de predicat a luiwait()sau să o includeți într-o buclăwhile:
Bucla ```cpp
// CORRECT — predicate form handles spurious wakeups
cv.wait(lock, []{ return ready; });
// EQUIVALENT manual loop
while (!ready) {
cv.wait(lock);
}
// WRONG — wakes up randomly, proceeds without data
cv.wait(lock); // no condition check!
```
Never call cv.wait(lock) without a predicate or a surrounding while.
Thread-Safe Bounded Buffer (producător-consumator)
Modelul producător-consumator este unul dintre cele mai comune modele de concurență. Producătorii adaugă articole într-un buffer partajat; consumatorii le elimina. Un buffer delimitat blochează producătorii când este plin și consumatorii când este gol.
#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;
}Problema trezirii pierdute
O trezire pierdută apare atunci cândnotify_one()este apelat înainte ca consumatorul să sunewait(). Deoarece niciun fir nu așteaptă, notificarea se pierde - apoi consumatorul doarme pentru totdeauna.
Modelul standard previne acest lucru prin verificarea predicatului inainte de dormit. Dacă producătorul a setat deja condiția la adevărat, predicatul returnează adevărat șiwait()revine imediat fără a dormi. Acesta este motivul pentru care forma predicatcv.wait(lock, pred)este esențială - verifică starea înainte și după fiecare trezire:
1. Blocați mutexul.
2. Verificați predicatul — dacă este deja adevărat, omiteți așteptarea.
3. În caz contrar, deblocați atomic și dormiți.
4. La trezire, blocați din nou și verificați din nou predicatul.
Atâta timp cât atât producătorul, cât și consumatorul protejează steagul partajat cu același mutex, nicio trezire nu poate fi pierdută.
notify_one vs notify_all
notify_one()trezește un fir în așteptare;notify_all()îi trezește pe toți. Utilizaținotify_all()atunci când mai multe fire de execuție ar putea avea nevoie să-și verifice din nou condițiile (de exemplu, semnalele de închidere) șinotify_one()atunci când trebuie să continue exact un fir de execuție (de exemplu, un singur consumator care preia o sarcină).
#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;
}Folosiți întotdeauna supraîncărcarea predicatuluiwait()pentru a vă proteja împotriva trezirilor false și a trezirilor pierdute. Deblocați mutex-ul înainte de a apelanotify_one()/notify_all()atunci când este posibil - acest lucru evită blocarea imediată a firului trezit pe mutex. Preferaținotify_one()când trebuie să se trezească un singur fir șinotify_all()pentru scenarii de difuzare, cum ar fi oprirea. Nu așteptați niciodată o variabilă de condiție cu unlock_guard- aveți nevoie deunique_lock.
std::condition_variablelasă firele de execuție să intre până când o condiție este adevărată, evitând așteptările costisitoare- Utilizați întotdeauna forma de predicat
wait()pentru a gestiona atât trezirile false, cât și trezirile pierdute - Modelul producător-consumator utilizează două variabile de condiție: „nu plin” și „nu gol”
notify_one()trezește un chelner;notify_all()trezește toți chelnerii — folosește-l pe cel potrivit pentru scenariul tău- Atât chelnerul, cât și notificatorul trebuie să folosească același mutex pentru a proteja condiția partajată
Test — Verifică-ți cunoștințele
(20 XP)1. De ce trebuie să folosiți întotdeauna un predicat cu`condition_variable::wait()`?
2. Într-o coadă limitată producător-consumator, ce ar trebui să facă producătorul când coada este plină?
3. Ce tip de blocare necesită`std::condition_variable::wait()`?