Ответ 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).
Эта гонка не существует в этом вопросе.