← Назад к вопросам

Для чего нужна std::condition_variable?

2.2 Middle🔥 131 комментариев
#Многопоточность и синхронизация

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Для чего нужна 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 автоматически:

  1. Отпускает мьютекс (unlock)
  2. Блокирует поток
  3. При пробуждении снова захватывает мьютекс (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Синхронизация нескольких потоковЖдать всех перед продолжением

Лучшие практики

  1. Всегда используй predicate в wait()

    // ✅ ХОРОШО
    cv.wait(lock, [] { return condition; });
    
    // ❌ ПЛОХО: spurious wakeup — ложное пробуждение
    cv.wait(lock);
    
  2. Используй unique_lock, а не lock_guard

  3. Уведомляй после изменения данных

    {
        unique_lock<mutex> lock(mtx);
        data = new_value;
    }  // Отпускаем блокировку
    cv.notify_one();  // Потом уведомляем
    
  4. Используй notify_one() для одного потока, notify_all() для всех

Вывод: std::condition_variable — это мощный инструмент для эффективной синхронизации потоков, избегающий busy-waiting и позволяющий потокам уведомлять друг друга о наступлении условий. Это критично для построения масштабируемых многопоточных приложений.