Семантика условных переменных в C ++ 11 - PullRequest
1 голос
/ 18 апреля 2020

Я пытаюсь понять семантику std :: condition_variable . Я думал, что у меня было приличное понимание модели параллелизма C ++ 11 (атомика, упорядочение памяти, соответствующие гарантии и формальные отношения ), но описание того, как правильно использовать условные переменные, кажется, противоречит моему пониманию .

TL; DR

Ссылка говорит:

Поток, который намеревается изменить переменную, имеет значение

  1. получить std :: mutex (обычно через std :: lock_guard)
  2. выполнить модификацию, пока удерживается блокировка
  3. выполнить notify_one или notify_all для std :: condition_variable ( блокировку не нужно удерживать для уведомления)

Даже если общая переменная - это atomi c, ее необходимо изменить в мьютексе, чтобы правильно опубликовать sh изменение ожидающего нить.

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

Более подробно

Если мое прочтение вышеизложенного верно, то почему это так? Представьте, что мы выполняем модификации перед критической секцией (не допуская гонки, путем правильного использования атомных и блокировок). Например,

std::atomic<bool> dummy;
std::mutex mtx;
std::condition_variable cv;

void thread1() {
    //...
    // Modify some program data, possibly in many places, over a long period of time
    dummy.store(true, std::memory_order_relaxed); // for simplicity
    //...
    mtx.lock(); mtx.unlock();
    cv.notify_one();
    //...
}

void thread2() {
    // ...
    { std::unique_lock<std::mutex> ul(mtx);
        cv.wait(ul, []() -> bool {
            // A complex condition, possibly involving data from many places
            return dummy.load(std::memory_order_relaxed); // for simplicity
        });
    }
    // ...
}

Насколько я понимаю, cv.wait() блокируется на mtx перед продолжением (чтобы проверить условие и выполнить остальную часть программы). Кроме того, std::mutex::lock() считается операцией acqu , а std::mutex::unlock() считается операцией release . Не означает ли это, что unlock () в thread1 синхронизируется с lock () в thread2, и, следовательно, все хранилища atomi c и даже не Atomi c выполняются в thread1 до unlock() видны для thread2, когда он просыпается?

Formally:  store --sequenced-before--> unlock() --synchronizes-with--> lock() --sequenced-before--> load
...and so: store --happens-before--> load

Большое спасибо за любые ответы!

[Примечание: я нахожу странным, что я не нашел ответа на этот вопрос после обширного прибегая к помощи; Извините, если это дубликат ...]

1 Ответ

1 голос
/ 19 апреля 2020

Рассмотрим время до блокировки мьютекса в thread1 и время до того, как condition_variable впервые разблокирует мьютекс в thread2.

thread1 делает

  • Изменить много данных программы
  • dummy.store(true, std::memory_order_relaxed)

thread2 делает

  • Блокировка мьютекса
  • dummy.load(std::memory_order_relaxed) (чтобы проверить предикат перед ожиданием)

Нет последовательности относительно друг друга. Если thread2 видит истинное значение для dummy при этой проверке и продолжает работу, нет никакой гарантии, что какие-либо изменения данных будут видны для thread2. thread2 продолжит работу, правильно увидев значение dummy, но не увидев корректных изменений.

Вы говорите: «Обеспечение отсутствия расы при правильном использовании атомных соединений и блокировок», что очень открыто. Расслабленная атомика будет правильной, а модификации не обязательно будут видны в thread2. Однако гипотетическая дополнительная синхронизация вокруг этих других модификаций данных может гарантировать видимость.

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

Это похоже на : ожидание в рабочем потоке с использованием флага std :: atomi c и переменной std :: condition_

...