Почему «ожидание с предикатом» решает «потерянное пробуждение» для условной переменной? - PullRequest
0 голосов
/ 30 мая 2019

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

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

#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;
}

1 Ответ

2 голосов
/ 30 мая 2019

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

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

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

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

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

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

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

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

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

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

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

...