Почему «ожидание с предикатом» решает «потерянное пробуждение» для условной переменной?

Я пытаюсь понять разницу между ложным и потерянным пробуждением в случае условной переменной. Ниже приведен небольшой фрагмент кода, который я пробовал. Я понимаю, что «потребитель» в этом случае может проснуться без какого-либо уведомления, и поэтому ожидание должно проверять наличие предиката.

Но как ожидание с предикатом решает проблему «потерянного пробуждения»? Как вы можете видеть в коде ниже; «подождать» не вызывается в течение 5 секунд, и я ожидал, что он пропустит первые несколько уведомлений; но с predate он ничего не пропускает. Сохраняются ли эти уведомления для будущего ожидания?

#include <iostream>
#include <deque>
#include <condition_variable>
#include <thread>

std::deque<int> q;
std::mutex m;
std::condition_variable cv;

void dump_q()
{
    for (auto x: q) {
        std::cout << x << std::endl;
    }
}

void producer()
{
    for(int i = 0; i < 10; i++) {
        std::unique_lock<std::mutex> locker(m);
        q.push_back(i);
        std::cout << "produced: " << i << std::endl;
        cv.notify_one();

        std::this_thread::sleep_for(std::chrono::seconds(1));
        locker.unlock();
    }
}

void consumer()
{
    while (true) {
        int data = 0;
        std::this_thread::sleep_for(std::chrono::seconds(5));   // <- should miss first 5 notications?
        std::unique_lock<std::mutex> locker(m); 
        cv.wait(locker);
        //cv.wait(locker, [](){return !q.empty();});  // <- this fixes both spurious and lost wakeups
        data = q.front();
        q.pop_front();
        std::cout << "--> consumed: " << data << std::endl;
        locker.unlock();
    }
}

int main(int argc, char *argv[])
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
0
1 666
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Это атомарная операция «разблокировать и ждать», которая предотвращает потерянные пробуждения. Потерянное пробуждение происходит следующим образом:

  1. Мы приобретаем замок, который защищает данные.
  2. Мы проверяем, нужно ли нам ждать, и видим, что ждем.
  3. Нам нужно снять блокировку, потому что иначе ни один другой поток не сможет получить доступ к данным.
  4. Мы ждем пробуждения.

Вы можете увидеть риск потери пробуждения здесь. Между шагами 3 и 4 другой поток может получить блокировку и отправить пробуждение. Мы сняли блокировку, поэтому это может сделать другой поток, но мы еще не ждем, поэтому не получим сигнал.

Пока шаг 2 выполняется под защитой блокировки, а шаги 3 и 4 являются атомарными, риск потери пробуждения отсутствует. Пробуждение не может быть отправлено до тех пор, пока данные не будут изменены, что невозможно сделать, пока другой поток не получит блокировку. Поскольку 3 и 4 являются атомарными, любой поток, который видит блокировку как разблокированную, обязательно также увидит нас в ожидании.

Эта атомарная «разблокировка и ожидание» является основной целью условных переменных и причиной, по которой они всегда должны быть связаны с мьютексом и предикатом.

In code above, consumer is not waiting for first few notifications because it is sleeping. Is it not missing notify in this case? Is this case not similar to race condition between #3 and #4?

Неа. Не может быть.

Либо потребитель, который не ждет, удерживает блокировку, либо нет. Если потребитель, который не ждет, держит блокировку, он ничего не пропустит. Предикат не может измениться, когда он удерживает блокировку.

Если потребитель не держит блокировку, то не имеет значения, что он упускает. Когда он проверяет, должен ли он блокироваться на шаге 2, если он что-то пропустил, он обязательно увидит это на шаге 2 и увидит, что ему не нужно ждать, поэтому он не будет ждать пробуждения, которое он пропустил.

Таким образом, если предикат таков, что потоку не нужно ждать, поток не будет ждать, потому что он проверяет предикат. Нет возможности пропустить пробуждение до шага 1.

Единственное время, когда действительное пробуждение необходимо, это когда поток переходит в спящий режим. Атомарная разблокировка и спящий режим гарантируют, что поток может принять решение о переходе в спящий режим только тогда, когда он удерживает блокировку, а то, чего ему нужно ждать, еще не произошло.

В приведенном выше коде потребитель не ждет первых нескольких уведомлений, потому что он находится в спящем режиме. В этом случае не пропало уведомление? Разве этот случай не похож на состояние гонки между № 3 и № 4? Спасибо за помощь.

bladeWalker 30.05.2019 22:26

@spa Я обновлю свой ответ. В этом случае шаг 2 невозможен, поэтому мы никогда не дойдем до шага 4.

David Schwartz 30.05.2019 22:38

@DavidSchwartz, извините, что задал этот вопрос поздно. Я все еще не уверен в одном. Вы говорите, что потребитель наверняка пропустит уведомления (из-за сна, std::this_thread::sleep_for(std::chrono::seconds(5));)? Но поскольку потребитель проверяет предикат, не имеет значения, пропустит ли он их?

awakened 14.02.2022 04:59

@Пробужденный Верно. Неважно, пропустит ли он пробуждение, если он все равно не ждал пробуждения в это время.

David Schwartz 14.02.2022 07:19

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