Переменные состояния и производитель-потребитель
Используйте переменные состояния для эффективной координации потоков. Постройте безопасную очередь производителя-потребителя, обработайте ложные пробуждения и избегайте потерянной проблемы пробуждения.
Координация потока с переменными состояния
переменная состояния позволяет потоку подождать, пока какое-то условие не станет истинным, без загруженности. Вместо вращения while (!ready) { /* burn CPU */ } нить спит и пробуждается другой нитью, которая называется notify_one() или notify_all().
std::condition_variable работает только с std::unique_lock. Рисунок всегда:
1. Заблокируйте мутекс.
2. Проверьте состояние — если оно ложное, позвоните в wait(), который атомарно высвобождает мутекс и приостанавливает поток.
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:
// 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!Никогда не звоните в cv.wait(lock) без сказуемого или окружающего цикла while.
Thread-Safe Bounded 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(). Поскольку нить не ждет, уведомление теряется — потребитель спит вечно.
Стандартная схема предотвращает это, проверяя предикат перед сном. Если производитель уже установил условие истинности, предикат возвращается истинным, а wait() возвращается немедленно без сна. Вот почему форма предиката cv.wait(lock, pred) имеет важное значение — она проверяет состояние до и после каждого пробуждения:
1. Заблокируйте мутекс.
2.Проверить предикат — если уже верно, пропустить ожидание.
3. В противном случае, атомарно разблокировать и спать.
4. Пробуждение, повторная блокировка и повторная проверка предиката.
До тех пор, пока производитель и потребитель защищают общий флаг с тем же самым мутексом, никакое пробуждение не может быть потеряно.
Notify one против 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_variableпозволяет нитям спать до тех пор, пока состояние не станет истинным, избегая дорогостоящего ожидания.- Всегда используйте форму предиката
wait()для обработки ложных и потерянных пробуждений. - Модель «производитель-потребитель» использует две переменные состояния: «не полный» и «не пустой».
notify_one()будит одного официанта;notify_all()будит всех официантов - используйте правильный для вашего сценария- И официант, и уведомитель должны использовать тот же mutex для защиты общего состояния.
Тест — проверьте свои знания
(20 XP)1. Почему вы всегда должны использовать предикат с `condition_variable::wait()`?
2. В ограниченной очереди «производитель-потребитель» что должен делать производитель, когда очередь заполнена?
3. Какой тип блокировки требуется `std::condition_variable::wait()`?