Вопрос об условии_переменной, почему условие_переменная соединено с мьютексом

Я учусь std::condition_variable недавно и есть вопрос, который я не могу понять.

Cppreference и многие другие руководства приводят такой пример:

std::mutex m;
std::condition_variable cv;

void worker_thread()
{
    // wait until main() sends data
    std::unique_lock lk(m);
    cv.wait(lk, []{ return ready; });
 
    // after the wait, we own the lock
    std::cout << "Worker thread is processing data\n";
    data += " after processing";
 
    // send data back to main()
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";
 
    // manual unlocking is done before notifying, to avoid waking up
    // the waiting thread only to block again (see notify_one for details)
    lk.unlock();
    cv.notify_one();
}
  1. Почему condition_variable всегда в паре с mutex? Я не могу понять, зачем здесь нужен мьютекс.
  2. Что здесь делает лямбда-функция []{ return ready; }?

Чтобы сделать неблокирующее ожидание потокобезопасным. Например, вы не хотите, чтобы предикат менял значение во время его оценки (оценка не является атомарной операцией). Предикат является обязательным, поскольку переменные условия могут ложно активироваться. Также знайте, что блокировка обычно «разблокирована», если условная переменная не выполняет часть своей работы.

Pepijn Kramer 26.08.2024 05:21

Вы показываете только половину примера. Вам не хватает второго потока, который фактически устанавливает условие. []{ return ready; } — это условие, которое вы хотите дождаться, т. е. оно ждет, пока ready не станет true.

user17732522 26.08.2024 05:23

Что также может помочь вашей ментальной модели: переменная условия — это не переменная, за которой ведется наблюдение, это сигнал о том, что что-то могло измениться (в другом потоке).

Pepijn Kramer 26.08.2024 05:24

«Почему условие_переменная сочетается с мьютексом» — это просто то, как оно устроено. Я не знаю наверняка, но считаю, что это наследие pthreads.

wohlstad 26.08.2024 06:33

Мой ответ здесь показывает полный пример использования std::condition_varibale в типичном случае производитель-потребитель.

wohlstad 26.08.2024 06:41
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
72
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

std::condition_variable существует как инструмент для обмена сообщениями низкого уровня. Вместо предоставления «семафора», «ворота» или других примитивов потоковой обработки библиотека C++ std предоставляет примитив низкого уровня, соответствующий тому, как работает аппаратная потоковая обработка, и который вы можете использовать для реализации этих других потоковых примитивов.

std::condition_variable предоставляет перехватчики для получения уведомлений, а std::mutex предоставляет перехватчики для защиты данных. Как это бывает, в реальном мире реального оборудования и ОС, предоставляемых примитивами уведомлений, аппаратные примитивы уведомлений не являются на 100% надежными, поэтому вам необходимо иметь некоторые данные для резервного копирования вашей системы уведомлений.

В частности, возможны ложные уведомления — может появиться уведомление, которое не соответствует отправке кем-либо уведомления на ваш std::condition_variable (или на его базовое оборудование/примитив ОС).

Поэтому, когда вы получаете уведомление, вы должны проверить некоторые данные (потокобезопасным способом), чтобы определить, действительно ли уведомление соответствует сообщению или нет.

В результате стандартный способ использования std::condition_variable состоит в том, чтобы иметь 3 тесно связанных между собой части:

  1. std::mutex, который защищает некоторые данные, содержащие возможное сообщение.
  2. std::condition_variable, который используется для передачи уведомления.
  3. Данные, содержащие само сообщение.

Действительно простая система сообщений могла бы представлять собой ворота, которые можно открывать и никогда не закрывать. Здесь ваши данные — это значок bool, в котором указано «открыты ли ворота». Когда вы открываете ворота, вы изменяете это логическое значение (потокобезопасным образом) и уведомляете всех, кто ожидает открытия ворот. Код, ожидающий открытия ворот, ожидает условную переменную; когда он просыпается, он проверяет, открыты ли ворота, и принимает уведомление только в том случае, если они действительно открыты. Если ворота закрыты, он считает пробуждение ложным и снова засыпает.

В реальном коде:

struct Gate {

  void open() {
    auto l = lock();
    is_open = true;
    cv.notify_all();
  }

  void wait() const {
    auto l = lock();
    cv.wait(l, [&]{return is_open;});
  }

private:
  mutable std::mutex m;
  bool is_open = false;
  mutable std::condition_variable cv;

  auto lock() const { return std::unique_lock{m}; }
}

в open мы блокируем мьютекс (поскольку мы редактируем общее состояние - bool), мы редактируем bool is_open, чтобы сказать, что он открыт, затем мы уведомляем всех, кто ожидает его открытия, что он действительно открыт.

На стороне wait нам нужен мьютекс для вызова cv.wait. Затем мы ждем, пока ворота не откроются — лямбда-версия wait создает для нас цикл, который проверяет наличие ложных пробуждений и снова переходит в режим сна, когда они происходят.

Лямбда-версия:

 auto l = lock();
 cv.wait(l, [&]{return is_open;});

это просто сокращение для:

 auto l = lock();
 while (!is_open)
   cv.wait(l);

то есть небольшой «цикл ожидания», который ищет is_open, чтобы быть правдой.

Без cv.wait(l) код был бы замкнутым циклом (ну, вам также хотелось бы разблокировать l и повторно заблокировать его). С помощью cv.wait(l) он разблокирует мьютекс m и будет ждать появления notify; нить не будет крутиться. Когда происходит notify (или иногда ложно - без какой-либо причины), он просыпается, повторно получает блокировку l на мьютексе m, а затем проверяет is_open. Если он действительно открыт, он выйдет из функции; если оно не открыто, оно сочтет это ложным уведомлением и зациклится.

Используя этот примитив условной переменной, вы можете написать целую кучу примитивов для уведомлений — потокобезопасные очереди сообщений, гейты, семафоры, системы многофункционального ожидания и т. д.

Мой любимый инструмент для этого выглядит так:

template<class T>
struct mutex_guarded {
  auto read(auto f) const -> decltype( f(std::declval<T const&>()) );
  auto write(auto f) -> decltype( f(std::declval<T&>()) );
private:
  mutable std::mutex m;
  T t;
};

это маленькая псевдомонада, которая оборачивает произвольный объект типа T во мьютекс. Затем мы расширяем его:

enum class notify {
  none,
  one,
  all
};

template<class T>
struct notifier:mutex_guarded {
  notify maybe_notify(auto f); // f(T&)->notify
  void wait(auto f) const; // f(T const&)->bool
  bool wait_for(auto f, auto duration) const; // f(T const&)->bool
  bool wait_until(auto f, auto time_point) const; // f(T const&)->bool
private:
  mutable std::condition_variable cv;
};

здесь мы оборачиваем код уведомления как в псевдомонаде.

notifier<bool> gate;
// the open() method is:
gate.maybe_notify([](bool& open){open=true; return notify::all;});
// the wait() method is:
gate.wait([](bool open){return open;});

Это охватывает около 99% случаев использования std::condition_variable и std::mutex; оставшийся 1%... более продвинутый.

Другие вопросы по теме