Что такое std::condition_variable в C++?

Ответ

std::condition_variable — это примитив синхронизации из стандартной библиотеки C++ (<condition_variable>), который позволяет одному или нескольким потокам ожидать (блокироваться) до тех пор, пока не будет выполнено некоторое условие, или пока их не уведомит другой поток. Он всегда используется в паре с std::mutex для защиты общего условия от гонок данных.

Основные методы:

  • wait(std::unique_lock<std::mutex>& lock, Predicate pred): Блокирует текущий поток. Автоматически освобождает lock и ждет уведомления. После пробуждения повторно захватывает lock и проверяет предикат pred. Если предикат возвращает false, поток снова засыпает. Это защищает от ложных пробуждений (spurious wakeups).
  • notify_one(): Пробуждает один из ожидающих потоков (если такие есть).
  • notify_all(): Пробуждает все ожидающие потоки.

Типичный паттерн "Ожидание условия":

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>

std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;
int shared_data = 0;

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    // Ждем, пока флаг data_ready не станет true.
    cv.wait(lock, []{ return data_ready; });
    // После пробуждения мьютекс снова захвачен, данные безопасны.
    std::cout << "Consumer received: " << shared_data << 'n';
}

void producer() {
    // Имитируем подготовку данных
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    {
        std::lock_guard<std::mutex> lock(mtx);
        shared_data = 42;
        data_ready = true;
        std::cout << "Producer sent data.n";
    } // Мьютекс освобождается здесь
    cv.notify_one(); // Уведомляем потребителя
}

int main() {
    std::thread t1(consumer);
    std::thread t2(producer);
    t1.join();
    t2.join();
    return 0;
}

Важные замечания:

  1. Всегда используйте std::unique_lock с condition_variable, так как wait() должен уметь временно отпускать и снова захватывать мьютекс.
  2. Изменение условия (data_ready) и вызов notify_*() должны выполняться под защитой того же мьютекса, чтобы избежать состояния гонки, когда уведомление отправляется до того, как потребитель начал ждать (потерянное уведомление).
  3. Предикат в wait() — это не просто синтаксический сахар, а необходимая защита от ложных пробуждений, которые разрешены стандартом.

Ответ 18+ 🔞

А, condition_variable, ёпта! Ну это ж классика, ебать мои старые костыли. Представь себе такую картину: у тебя есть поток, который сидит и ждёт, как дурак, пока ему не принесут пиццу. А другой поток — это курьер, который её готовит. Так вот std::condition_variable — это такая дверь с глазком, в которую твой ждущий поток смотрит и орёт: «Ну чё там, готово, блядь?!»

Работает это всегда в паре с мьютексом (std::mutex), потому что иначе начнётся пиздец: пока один смотрит в глазок, другой может в дверь пиццей въехать, и все данные разъебутся.

Основные фишки:

  • wait(lock, pred) — это когда поток говорит: «Ладно, я пока посплю, разбудите, когда будет готово». Он отпускает мьютекс (чтобы другие могли зайти на кухню-то, данные поменять) и отрубается. А pred — это такая хитрая жопа, проверка. Потому что стандарт разрешает потоку проснуться просто так, от ветра, это называется «ложное пробуждение». Поэтому проснулся — сразу смотри: «А пицца-то моя приехала? Нет? Ну тогда пошёл нахуй, сплю дальше».
  • notify_one() — это тихонько пошептать одному спящему: «Эй, чувак, может, твоя пицца готова?». Разбудит только одного.
  • notify_all() — это встать посреди комнаты и орать: «ВСЕМ ПИЦЦА, БЛЯДЬ!». Проснутся все, но тот, чья пицца готова, её заберёт, а остальные снова уснут, материясь.

Типичная схема, когда один ждёт, другой делает:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>

std::mutex mtx; // Замок на кухне
std::condition_variable cv; // Глазок в дверь
bool data_ready = false; // Флаг "пицца готова"
int shared_data = 0; // Сама пицца

void consumer() {
    std::unique_lock<std::mutex> lock(mtx); // Захватил кухню, смотрю
    // Жду, пока флаг не станет true. Лямбда — это проверка на готовность.
    cv.wait(lock, []{ return data_ready; });
    // О, проснулся, мьютекс снова у меня в лапах, можно безопасно жрать!
    std::cout << "Consumer received: " << shared_data << 'n';
}

void producer() {
    // Имитируем, что готовим пиццу (задержка)
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    {
        std::lock_guard<std::mutex> lock(mtx); // Захватываем кухню, чтобы положить пиццу
        shared_data = 42; // Кладём пиццу
        data_ready = true; // Ставим флажок "ГОТОВО"
        std::cout << "Producer sent data.n";
    } // Здесь lock_guard разрушается и отпускает мьютекс
    cv.notify_one(); // Стучим в глазок: «Эй, жирный, твоя пицца!»
}

int main() {
    std::thread t1(consumer);
    std::thread t2(producer);
    t1.join();
    t2.join();
    return 0;
}

Важные моменты, где все ебутся:

  1. Используй ТОЛЬКО std::unique_lock. Потому что wait() должен уметь отпустить мьютекс на время сна, а потом схватить его обратно. lock_guard на это не способен, он тупой как пробка.
  2. Менять общее условие (data_ready) и орать notify_*() нужно обязательно под тем же мьютексом. Иначе будет race condition: представь, ты поставил флаг data_ready = true и крикнул notify_one(), а потребитель в этот момент ещё даже не начал ждать и не услышит крик. Уведомление потеряется, и он будет спать вечно, ядрёна вошь!
  3. Предикат в wait() — это не прихоть, а суровая необходимость. Потому что потоки могут просыпаться сами по себе, без всякого notify. Это как если бы тебя ночью разбудил соседский кот. Ты встаёшь, смотришь — пиццы нет. И идёшь дальше спать. Без предиката ты бы подумал, что пицца пришла, и пошёл бы есть воздух.