Подтвердить что ты не робот

Должен ли я получить блокировку перед вызовом condition_variable.notify_one()?

Я немного запутался в использовании std::condition_variable. Я понимаю, что перед тем, как позвонить condition_variable.wait(), мне нужно создать unique_lock на mutex. Я не могу найти, должен ли я также получить уникальный замок перед вызовом notify_one() или notify_all().

Примеры cppreference.com противоречивы. Например, страница notify_one дает следующий пример:

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

std::condition_variable cv;
std::mutex cv_m;
int i = 0;
bool done = false;

void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cout << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    std::cout << "...finished waiting. i == 1\n";
    done = true;
}

void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Notifying...\n";
    cv.notify_one();

    std::unique_lock<std::mutex> lk(cv_m);
    i = 1;
    while (!done) {
        lk.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
        lk.lock();
        std::cerr << "Notifying again...\n";
        cv.notify_one();
    }
}

int main()
{
    std::thread t1(waits), t2(signals);
    t1.join(); t2.join();
}

Здесь блокировка не получена для первого notify_one(), но приобретается для второго notify_one(). Глядя на другие страницы с примерами, я вижу разные вещи, в основном не приобретая замок.

  • Могу ли я сам выбрать блокировку мьютекса перед вызовом notify_one(), и почему я решил заблокировать его?
  • В приведенном примере, почему нет блокировки для первого notify_one(), но для последующих вызовов. Является ли этот пример неправильным или есть какое-то обоснование?
4b9b3361

Ответ 1

Вам не нужно удерживать блокировку при вызове condition_variable::notify_one(), но это не так в том смысле, что оно все еще четко определено поведение, а не ошибка.

Тем не менее, это может быть "пессимизация", поскольку любой ожидающий поток становится исполняемым (если он есть) немедленно попытается получить блокировку, выполняемую уведомляющим потоком. Я считаю хорошим правилом, чтобы избежать блокировки, связанной с переменной состояния при вызове notify_one() или notify_all(). См. Puteread Mutex: pthread_mutex_unlock() потребляет много времени для примера, когда освобождение блокировки перед вызовом эквивалента pthread notify_one() заметно улучшилось.

Имейте в виду, что вызов lock() в цикле while необходим в какой-то момент, потому что блокировка должна быть сохранена во время проверки состояния цикла while (!done). Но его не нужно удерживать для вызова notify_one().


2016-02-27. Большое обновление, чтобы ответить на некоторые вопросы в комментариях о том, является ли условие гонки состоянием блокировки, не помогает для вызова notify_one(). Я знаю, что это обновление задерживается, потому что вопрос задавался почти два года назад, но я хотел бы задать вопрос @Cookie о возможном состоянии гонки, если производитель (signals() в этом примере) называет notify_one() непосредственно перед потребителем (waits() в этом примере) может вызвать wait().

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

Производителю необходимо удерживать блокировку при обновлении i, а потребитель должен удерживать блокировку при проверке i и вызывать condition_variable::wait() (если ему вообще нужно ждать). В этом случае ключ состоит в том, что он должен быть одним и тем же экземпляром блокировки (часто называемой критической секцией), когда потребитель выполняет эту проверку и ожидание. Поскольку критический раздел хранится, когда продюсер обновляет i, и когда потребитель проверяет и ждет на i, нет возможности для i изменить между тем, когда потребитель проверяет i и когда он вызывает condition_variable::wait(). Это суть правильного использования переменных условия.

В стандарте С++ указано, что condition_variable:: wait() ведет себя следующим образом при вызове с предикатом (как в этом случае):

while (!pred())
    wait(lock);

Существует две ситуации, когда потребитель проверяет i:

  • если i равно 0, то потребитель вызывает cv.wait(), тогда i будет по-прежнему 0, когда будет вызвана часть реализации wait(lock) - правильное использование блокировок гарантирует это. В этом случае у продюсера нет возможности вызывать condition_variable::notify_one() в цикле while до тех пор, пока пользователь не вызвал cv.wait(lk, []{return i == 1;}) (и вызов wait() сделал все, что ему нужно сделать, чтобы правильно "уловить" уведомление - wait() не отпустит блокировку до тех пор, пока она не сделает это). Поэтому в этом случае потребитель не может пропустить уведомление.

  • если i уже 1, когда потребитель вызывает cv.wait(), часть реализации wait(lock) никогда не будет вызвана, потому что тест while (!pred()) приведет к завершению внутреннего цикла. В этой ситуации не имеет значения, когда возникает вызов notify_one() - потребитель не будет блокироваться.

В приведенном ниже примере есть дополнительная сложность использования переменной done, которая возвращает сигнал к потоку производителя, который потребитель узнал, что i == 1, но я не думаю, что это вообще меняет анализ, потому что все доступ к done (как для чтения, так и для изменения) выполняется в тех же критических разделах, которые включают i и condition_variable.

Если вы посмотрите на вопрос, на который @eh9 указал, Синхронизация ненадежна с использованием std:: atomic и std:: condition_variable, вы увидите условие гонки. Однако код, отправленный в этом вопросе, нарушает одно из основных правил использования переменной условия: при выполнении проверки и ожидания не выполняется ни один критический раздел.

В этом примере код выглядит так:

if (--f->counter == 0)      // (1)
    // we have zeroed this fence counter, wake up everyone that waits
    f->resume.notify_all(); // (2)
else
{
    unique_lock<mutex> lock(f->resume_mutex);
    f->resume.wait(lock);   // (3)
}

Вы заметите, что wait() на # 3 выполняется, удерживая f->resume_mutex. Но проверка того, нужен или нет wait() на шаге 1, не выполняется при сохранении этой блокировки вообще (гораздо менее непрерывно для проверки и ожидания), что является требованием для правильного использования переменных условия), Я считаю, что человек, у которого есть проблема с этим фрагментом кода, думал, что, поскольку f->counter был std::atomic, это выполнило бы это требование. Однако атомарность, предоставляемая std::atomic, не распространяется на последующий вызов f->resume.wait(lock). В этом примере есть пробег между тем, когда f->counter проверяется (шаг # 1) и когда вызывается wait() (шаг № 3).

Эта гонка не существует в этом вопросе.

Ответ 2

Ситуация

Используя vc10 и Boost 1.56, я реализовал параллельную очередь, похожую на в этом сообщении в блоге. Автор разблокирует мьютекс, чтобы свести к минимуму конфликт, т.е. notify_one() вызывается с разблокированным мьютексом:

void push(const T& item)
{
  std::unique_lock<std::mutex> mlock(mutex_);
  queue_.push(item);
  mlock.unlock();     // unlock before notificiation to minimize mutex contention
  cond_.notify_one(); // notify one waiting thread
}

Разблокировка мьютекса поддерживается примером в Документация Boost:

void prepare_data_for_processing()
{
    retrieve_data();
    prepare_data();
    {
        boost::lock_guard<boost::mutex> lock(mut);
        data_ready=true;
    }
    cond.notify_one();
}

Проблема

Тем не менее это привело к следующему неустойчивому поведению:

  • пока notify_one() уже вызвано not cond_.wait() все еще может быть прервано через boost::thread::interrupt()
  • один раз notify_one() был вызван в первый раз cond_.wait() тупиков; ожидание не может завершиться boost::thread::interrupt() или boost::condition_variable::notify_*() больше.

Решение

Удаление строки mlock.unlock() заставило код работать как ожидалось (уведомления и прерывания заканчивают ожидание). Обратите внимание, что notify_one() вызывается с блокировкой мьютекса, после того, как он выходит из области действия, он разблокируется:

void push(const T& item)
{
  std::lock_guard<std::mutex> mlock(mutex_);
  queue_.push(item);
  cond_.notify_one(); // notify one waiting thread
}

Это означает, что, по крайней мере, с моей конкретной реализацией потока, мьютекс не должен быть разблокирован перед вызовом boost::condition_variable::notify_one(), хотя оба способа кажутся правильными.

Ответ 3

@Майкл Берр прав. condition_variable::notify_one не требует блокировки переменной. Ничто не мешает вам использовать блокировку в этой ситуации, хотя, как показывает этот пример.

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

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