Просто добавляю этот ответ, потому что я думаю, что принятый ответ может вводить в заблуждение. Во всех случаях вам нужно будет заблокировать мьютекс перед вызовом notify_one () где-нибудь, чтобы ваш код был потокобезопасным, хотя вы можете разблокировать его снова, прежде чем фактически вызывать notify _ * ().
Чтобы уточнить, вы ДОЛЖНЫ взять блокировку перед вводом wait (lk), потому что wait () разблокирует lk, и это будет Undefined Behavior, если блокировка не заблокирована. Это не относится к notify_one (), но вам нужно убедиться, что вы не вызываете notify _ * () перед вводом wait () и разблокировкой мьютекса этим вызовом; что, очевидно, можно сделать только заблокировав тот же мьютекс перед вызовом notify _ * ().
Например, рассмотрим следующий случай:
std::atomic_int count;
std::mutex cancel_mutex;
std::condition_variable cancel_cv;
void stop()
{
if (count.fetch_sub(1) == -999)
cv.notify_one();
}
bool start()
{
if (count.fetch_add(1) >= 0)
return true;
stop();
return false;
}
void cancel()
{
if (count.fetch_sub(1000) == 0)
return;
std::unique_lock<std::mutex> lk(cancel_mutex);
cancel_cv.wait(lk);
}
Предупреждение : этот код содержит ошибку.
Идея заключается в следующем: потоки вызывают start () и stop () попарно, но только до тех пор, пока start () возвращает true. Например:
if (start())
{
stop();
}
Один (другой) поток в какой-то момент вызовет cancel () и после возврата из cancel () уничтожит объекты, которые необходимы в «Делать что-нибудь». Однако предполагается, что cancel () не будет возвращаться, пока есть потоки между start () и stop (), и после того, как cancel () выполнил свою первую строку, start () всегда будет возвращать false, поэтому новые потоки не будут вводить 'Do зона вещей.
Работает правильно?
Рассуждения таковы:
1) Если какой-либо поток успешно выполняет первую строку start () (и, следовательно, вернет истину), тогда ни один поток еще не выполнил первую строку cancel () (мы предполагаем, что общее количество потоков намного меньше 1000 на путь).
2) Кроме того, хотя поток успешно выполнил первую строку start (), но еще не первую строку stop (), невозможно, чтобы какой-либо поток успешно выполнил первую строку cancel () (обратите внимание, что только один поток когда-либо вызывает cancel ()): значение, возвращаемое fetch_sub (1000), будет больше 0.
3) После того, как поток выполнил первую строку cancel (), первая строка start () всегда будет возвращать false, а поток, вызывающий start (), больше не будет входить в область «Что делать».
4) Количество вызовов start () и stop () всегда сбалансировано, поэтому после неудачного выполнения первой строки cancel () всегда будет момент, когда (последний) вызов stop () вызывает счет чтобы достичь -1000 и, следовательно, вызвать notify_one (). Обратите внимание, что это может произойти только тогда, когда первая строка отмены привела к провалу этого потока.
Помимо проблемы голодания, когда так много потоков вызывают start () / stop (), что count никогда не достигает -1000 и cancel () никогда не возвращает, что можно принять как «маловероятно и никогда не длится долго», есть еще одна ошибка:
Вполне возможно, что внутри области «Что делать» есть один поток, допустим, он просто вызывает stop (); в этот момент поток выполняет первую строку cancel (), считывая значение 1 с помощью fetch_sub (1000) и пропуская. Но прежде чем он возьмет мьютекс и / или вызовет ожидание (lk), первый поток выполняет первую строку stop (), считывает -999 и вызывает cv.notify_one ()!
Затем этот вызов notify_one () выполняется ДО того, как мы ждем () в переменной условия! И программа зашла бы в тупик на неопределенное время.
По этой причине мы не сможем вызвать notify_one (), пока не вызовем wait (). Обратите внимание, что сила условной переменной заключается в том, что она может атомарно разблокировать мьютекс, проверить, произошел ли вызов notify_one (), и перейти в режим сна. Вы не можете обмануть его, но делать нужно держать мьютекс заблокирован всякий раз , когда вы вносите изменения в переменных , которые могут изменить состояние от ложного к истине и держать его взаперти при вызове notify_one () из - за условий гонки , как описано здесь.
Однако в этом примере нет условия. Почему я не использовал в качестве условия «count == -1000»? Потому что здесь это совсем не интересно: как только будет достигнуто значение -1000, мы уверены, что ни один новый поток не войдет в область «Что делать». Более того, потоки по-прежнему могут вызывать start () и увеличивать счетчик (до -999 и -998 и т. Д.), Но нас это не волнует. Единственное, что имеет значение, это то, что было достигнуто значение -1000, чтобы мы точно знали, что в области «Что делать» больше нет потоков. Мы уверены, что это так, когда вызывается notify_one (), но как убедиться, что мы не вызываем notify_one () до того, как cancel () заблокирует его мьютекс? Простая блокировка cancel_mutex незадолго до notify_one (), конечно, не поможет.
Проблема заключается в том, что, несмотря на то что мы не ждем условия, там еще есть условие, и нам нужно заблокировать мьютекс
1) до достижения этого условия 2) до вызова notify_one.
Таким образом, правильный код становится:
void stop()
{
if (count.fetch_sub(1) == -999)
{
cancel_mutex.lock();
cancel_mutex.unlock();
cv.notify_one();
}
}
[... то же начало () ...]
void cancel()
{
std::unique_lock<std::mutex> lk(cancel_mutex);
if (count.fetch_sub(1000) == 0)
return;
cancel_cv.wait(lk);
}
Конечно, это только один пример, но другие случаи очень похожи; почти во всех случаях, когда вы используете условную переменную, вам нужно будет заблокировать этот мьютекс (вскоре) перед вызовом notify_one (), иначе вы можете вызвать его перед вызовом wait ().
Обратите внимание, что в этом случае я разблокировал мьютекс до вызова notify_one (), потому что в противном случае существует (небольшая) вероятность, что вызов notify_one () разбудит поток, ожидающий переменной условия, который затем попытается принять мьютекс и block, прежде чем снова освободить мьютекс. Это немного медленнее, чем нужно.
Этот пример был особенным в том смысле, что строка, изменяющая условие, выполняется тем же потоком, который вызывает wait ().
Более обычным является случай, когда один поток просто ожидает, пока условие станет истинным, а другой поток берет блокировку перед изменением переменных, участвующих в этом условии (в результате чего, возможно, оно станет истинным). В этом случае мьютекс будет немедленно заблокирован до того (и после) условия сбылось - так это совершенно нормально , чтобы просто разблокировать мьютекс перед вызовом уведомит _ * () в этом случае.
wait morphing
оптимизации) Эмпирическое правило, описанное в этой ссылке: notify WITH lock лучше использовать в ситуациях с более чем 2 потоками для более предсказуемых результатов.