Для чего нужна std::condition_variable?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Для чего нужна std::condition_variable?
std::condition_variable — это примитив синхронизации, который позволяет потокам сообщать друг другу о наступлении определённых условий. Она используется для координации работы между потоками, когда один поток должен ждать, пока другой поток выполнит какое-то действие или выполнится некоторое условие.
Основная проблема без condition_variable
Без condition_variable пришлось бы использовать busy-waiting (активное ожидание) — поток постоянно проверял бы условие в цикле:
#include <mutex>
#include <thread>
using namespace std;
bool ready = false;
mutex mtx;
void worker() {
this_thread::sleep_for(chrono::seconds(1));
{
lock_guard<mutex> lock(mtx);
ready = true;
}
}
int main() {
thread t(worker);
// ❌ ПЛОХО: busy-waiting — впустую тратит CPU
while (true) {
{
lock_guard<mutex> lock(mtx);
if (ready) break;
}
// Поток постоянно работает, потребляет CPU!
}
t.join();
return 0;
}
Этот подход неэффективен: поток ненужно потребляет CPU, блокирует мьютекс, не даёт другим потокам работать.
Решение: std::condition_variable
std::condition_variable позволяет потокам эффективно ждать без busy-waiting:
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>
using namespace std;
bool ready = false;
mutex mtx;
condition_variable cv;
void worker() {
this_thread::sleep_for(chrono::seconds(1));
{
lock_guard<mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // Уведомляем ждущий поток
}
int main() {
thread t(worker);
{
unique_lock<mutex> lock(mtx);
// ✅ ХОРОШО: поток блокируется, не потребляет CPU
cv.wait(lock, [] { return ready; });
}
cout << "Ready!" << endl;
t.join();
return 0;
}
Основные методы condition_variable
1. wait() — ждать условия
void wait(unique_lock<mutex>& lock);
void wait(unique_lock<mutex>& lock, Predicate pred);
bool wait_for(unique_lock<mutex>& lock, duration timeout);
bool wait_until(unique_lock<mutex>& lock, time_point deadline);
2. notify_one() — уведомить один поток
condition_variable cv;
mutex mtx;
// Поток 1: ждёт
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock);
// Поток 2: пробуждает
{
std::lock_guard<std::mutex> lock(mtx);
cv.notify_one(); // Пробуждает ровно один ждущий поток
}
3. notify_all() — уведомить все потоки
// Пробуждает все потоки, ожидающие на этой переменной
cv.notify_all();
Важная деталь: почему нужен unique_lock?
std::condition_variable требует std::unique_lock, а не std::lock_guard, потому что нужно управлять блокировкой внутри wait():
// ❌ ОШИБКА: std::lock_guard не поддерживает unlock/lock
mutex mtx;
condition_variable cv;
lock_guard<mutex> lock(mtx); // Нет методов unlock/lock
cv.wait(lock); // Ошибка компиляции!
// ✅ ПРАВИЛЬНО: std::unique_lock
unique_lock<mutex> lock(mtx);
cv.wait(lock); // OK, может вызвать unlock/lock внутри
Внутри wait() condition_variable автоматически:
- Отпускает мьютекс (unlock)
- Блокирует поток
- При пробуждении снова захватывает мьютекс (lock)
Практические примеры
Пример 1: Producer-Consumer (очередь данных)
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>
#include <iostream>
using namespace std;
queue<int> buffer;
mutex mtx;
condition_variable not_empty;
const int MAX_SIZE = 10;
void producer(int id) {
for (int i = 0; i < 5; ++i) {
unique_lock<mutex> lock(mtx);
buffer.push(id * 10 + i);
cout << "Produced: " << (id * 10 + i) << endl;
lock.unlock();
not_empty.notify_one(); // Уведомляем consumer
}
}
void consumer(int id) {
while (true) {
unique_lock<mutex> lock(mtx);
not_empty.wait(lock, [] { return !buffer.empty(); });
int value = buffer.front();
buffer.pop();
cout << "Consumer " << id << " got: " << value << endl;
}
}
int main() {
thread p1(producer, 1), p2(producer, 2);
thread c1(consumer, 1), c2(consumer, 2);
p1.join();
p2.join();
return 0;
}
Пример 2: Ждать с таймаутом
#include <mutex>
#include <condition_variable>
#include <chrono>
using namespace std;
bool data_ready = false;
mutex mtx;
condition_variable cv;
void wait_with_timeout() {
unique_lock<mutex> lock(mtx);
// Ждём максимум 5 секунд
if (cv.wait_for(lock, chrono::seconds(5),
[] { return data_ready; })) {
cout << "Data is ready!" << endl;
} else {
cout << "Timeout!" << endl;
}
}
Пример 3: Многопоточный пул задач
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>
#include <functional>
using namespace std;
class ThreadPool {
private:
queue<function<void()>> tasks;
vector<thread> workers;
mutex mtx;
condition_variable cv;
bool stop = false;
void worker_thread() {
while (true) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this] { return !tasks.empty() || stop; });
if (tasks.empty() && stop) break;
auto task = tasks.front();
tasks.pop();
lock.unlock();
task(); // Выполняем задачу
}
}
public:
ThreadPool(size_t num_workers) {
for (size_t i = 0; i < num_workers; ++i) {
workers.emplace_back(&ThreadPool::worker_thread, this);
}
}
void enqueue(function<void()> task) {
{
unique_lock<mutex> lock(mtx);
tasks.push(task);
}
cv.notify_one();
}
~ThreadPool() {
{
unique_lock<mutex> lock(mtx);
stop = true;
}
cv.notify_all();
for (auto& t : workers) {
t.join();
}
}
};
Сравнение с другими примитивами
| Примитив | Назначение | Использование |
|---|---|---|
| mutex | Взаимное исключение | Защита критических секций |
| condition_variable | Сигнализация между потоками | Producer-Consumer, Worker pools |
| semaphore | Ограничение ресурсов | Контроль максимума потоков |
| barrier | Синхронизация нескольких потоков | Ждать всех перед продолжением |
Лучшие практики
-
Всегда используй predicate в wait()
// ✅ ХОРОШО cv.wait(lock, [] { return condition; }); // ❌ ПЛОХО: spurious wakeup — ложное пробуждение cv.wait(lock); -
Используй unique_lock, а не lock_guard
-
Уведомляй после изменения данных
{ unique_lock<mutex> lock(mtx); data = new_value; } // Отпускаем блокировку cv.notify_one(); // Потом уведомляем -
Используй notify_one() для одного потока, notify_all() для всех
Вывод: std::condition_variable — это мощный инструмент для эффективной синхронизации потоков, избегающий busy-waiting и позволяющий потокам уведомлять друг друга о наступлении условий. Это критично для построения масштабируемых многопоточных приложений.