Μεταβλητές Κατάστασης & Παραγωγός-Καταναλωτής
Χρησιμοποιήστε μεταβλητές συνθήκης για να συντονίσετε αποτελεσματικά τα νήματα. Δημιουργήστε μια ασφαλή ουρά παραγωγού-καταναλωτή, χειριστείτε τις ψευδείς αφυπνίσεις και αποφύγετε το χαμένο πρόβλημα αφύπνισης.
Συντονισμός νήματος με μεταβλητές συνθήκης
Μια μεταβλητή συνθήκης επιτρέπει σε ένα νήμα να περιμένει έως ότου κάποια συνθήκη γίνει αληθής, χωρίς busy-looping. Αντί να περιστρέφεται τοwhile (!ready) { /* burn CPU */ }, το νήμα κοιμάται και ξυπνά από ένα άλλο νήμα που καλεί τοnotify_one()ή τοnotify_all().
Τοstd::condition_variableλειτουργεί μόνο με τοstd::unique_lock. Το μοτίβο είναι πάντα:
1. Κλειδώστε το mutex.
2. Ελέγξτε την κατάσταση — εάν είναι λάθος, καλέστε τοwait()που απελευθερώνει ατομικά το mutex και αιωρεί το νήμα.
3. Όταν ειδοποιηθεί (και η συνθήκη είναι αληθής), το mutex αποκτάται ξανά και η εκτέλεση συνεχίζεται.
Το νήμα ειδοποίησης τροποποιεί την κοινή κατάσταση στο ίδιο mutex και, στη συνέχεια, καλεί τοnotify_one()ή τοnotify_all().
Χρήση μεταβλητής βασικής συνθήκης
Εδώ είναι το θεμελιώδες μοτίβο: ένα νήμα περιμένει για δεδομένα, ένα άλλο τα παρέχει.
#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;
}Μια μεταβλητή συνθήκης μπορεί να ξυπνήσει ένα νήμα ακόμα και όταν δεν έχει σταλεί ειδοποίηση — αυτό ονομάζεται ψευδής αφύπνιση. Επιτρέπεται από το πρότυπο C++ (και κοινό σε πραγματικά συστήματα) για λόγους απόδοσης. Πρέπει πάντα να χρησιμοποιείτε την κατηγορούμενη μορφή τουwait()ή να την τυλίξετε σε έναν βρόχοwhile:
Βρόχος ```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.
Ενδιάμεση μνήμη με περιορισμένο νήμα (Παραγωγός-Καταναλωτής)
Το μοτίβο παραγωγός-καταναλωτής είναι ένα από τα πιο κοινά μοτίβα ταυτόχρονης χρήσης. Οι παραγωγοί προσθέτουν στοιχεία σε ένα κοινό buffer. οι καταναλωτές τα αφαιρούν. Ένα οριοθετημένο buffer μπλοκάρει τους παραγωγούς όταν είναι γεμάτο και τους καταναλωτές όταν είναι άδειο.
#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;
}Το πρόβλημα του χαμένου ξυπνήματος
Μια χαμένη αφύπνιση εμφανίζεται όταν τοnotify_one()καλείται πριν ο καταναλωτής καλέσει τοwait(). Εφόσον δεν περιμένει κανένα νήμα, η ειδοποίηση χάνεται — ο καταναλωτής κοιμάται για πάντα.
Το τυπικό μοτίβο το αποτρέπει ελέγχοντας το κατηγόρημα πριν ύπνο. Εάν ο παραγωγός έχει ήδη θέσει τη συνθήκη σε true, το κατηγόρημα επιστρέφει true και τοwait()επιστρέφει αμέσως χωρίς ύπνο. Αυτός είναι ο λόγος για τον οποίο η κατηγορούμενη μορφήcv.wait(lock, pred)είναι απαραίτητη — ελέγχει την κατάσταση πριν και μετά από κάθε αφύπνιση:
1. Κλειδώστε το mutex.
2. Ελέγξτε το κατηγόρημα — εάν είναι ήδη αληθές, παραλείψτε την αναμονή.
3. Διαφορετικά, ξεκλειδώστε ατομικά και κοιμηθείτε.
4. Κατά την αφύπνιση, κλειδώστε ξανά και ελέγξτε ξανά το κατηγόρημα.
Εφόσον τόσο ο παραγωγός όσο και ο καταναλωτής προστατεύουν την κοινόχρηστη σημαία με το ίδιο mutex, κανένα wakeup δεν μπορεί να χαθεί.
notify_one vs notify_all
Τοnotify_one()ενεργοποιεί ένα νήμα αναμονής. Τοnotify_all()ξυπνά όλες. Χρησιμοποιήστε τοnotify_all()όταν πολλά νήματα μπορεί να χρειαστεί να ελέγξουν ξανά τις συνθήκες τους (π.χ. σήματα τερματισμού λειτουργίας) καιnotify_one()όταν πρέπει να προχωρήσει ακριβώς ένα νήμα (π.χ. ένας μόνος καταναλωτής επιλέγει μια εργασία).
#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;
}Χρησιμοποιείτε πάντα την υπερφόρτωση κατηγορήματος τουwait()για να προστατεύεστε από ψευδείς ξυπνήσεις και χαμένες αφυπνίσεις. Ξεκλειδώστε το mutex πριν καλέσετε τοnotify_one()/notify_all()όταν είναι δυνατόν — έτσι αποφεύγεται το άμεσο μπλοκάρισμα του αφυπνισμένου νήματος στο mutex. Προτιμήστε τοnotify_one()όταν χρειάζεται να ενεργοποιηθεί μόνο ένα νήμα και τοnotify_all()για σενάρια μετάδοσης όπως ο τερματισμός λειτουργίας. Μην περιμένετε ποτέ σε μια μεταβλητή συνθήκης μεlock_guard— χρειάζεστεunique_lock.
std::condition_variablelets threads sleep until a condition is true, avoiding expensive busy-waiting- Always use the predicate form of
wait()to handle both spurious wakeups and lost wakeups - The producer-consumer pattern uses two condition variables: "not full" and "not empty"
notify_one()wakes one waiter;notify_all()wakes all waiters — use the right one for your scenario- Both the waiter and the notifier must use the same mutex to protect the shared condition
Κουίζ — Δοκιμάστε τις γνώσεις σας
(20 XP)1. Why must you always use a predicate with`condition_variable::wait()`?
2. In a bounded producer-consumer queue, what should the producer do when the queue is full?
3. Which lock type does`std::condition_variable::wait()`require?