С++ 11 std:: condition_variable: можем ли мы передать наш замок непосредственно в уведомляемый поток?
Я изучаю С++ 11 concurrency, где мой единственный предыдущий опыт с примитивами concurrency был в классе Operating Systems шесть лет назад, поэтому будьте осторожны, если сможете.
В С++ 11 мы можем написать
std::mutex m;
std::condition_variable cv;
std::queue<int> q;
void producer_thread() {
std::unique_lock<std::mutex> lock(m);
q.push(42);
cv.notify_one();
}
void consumer_thread() {
std::unique_lock<std::mutex> lock(m);
while (q.empty()) {
cv.wait(lock);
}
q.pop();
}
Это отлично работает, но я оскорблен необходимостью обернуть cv.wait
в цикле. Причина, по которой нам нужен цикл, мне понятна:
Consumer (inside wait()) Producer Vulture
release the lock
sleep until notified
acquire the lock
I MADE YOU A COOKIE
notify Consumer
release the lock
acquire the lock
NOM NOM NOM
release the lock
acquire the lock
return from wait()
HEY WHERE MY COOKIE I EATED IT
Теперь я считаю, что одна из интересных вещей о unique_lock
заключается в том, что мы можем передать ее, верно? Поэтому было бы очень элегантно, если бы мы могли это сделать:
Consumer (inside wait()) Producer
release the lock
sleep until notified
acquire the lock
I MADE YOU A COOKIE
notify and yield(passing the lock)
wake(receiving the lock)
return from wait()
YUM
release the lock
Теперь нет возможности вкрутить поток Vulture, потому что мьютекс остается заблокированным полностью от I MADE YOU A COOKIE
до YUM
. Кроме того, если notify()
требует, чтобы вы передали блокировку, это хороший способ убедиться, что люди фактически блокируют мьютекс перед вызовом notify()
(см. Сигнализация переменной условия (pthreads)).
Я уверен, что С++ 11 не имеет стандартной реализации этой идиомы. Какая историческая причина для этого (разве это просто так, что pthreads этого не делали, и почему это так)? Есть ли техническая причина, по которой авантюрный С++-кодер не смог реализовать эту идиому в стандартном С++ 11, назвав ее, возможно, my_better_condition_variable
?
У меня также есть смутное чувство, что, возможно, я изобретаю семафоры, но я недостаточно помню из школы, чтобы узнать, насколько это точно или нет.
Ответы
Ответ 1
Окончательный ответ заключается в том, что pthreads этого не делали. С++ - это язык, который инкапсулирует функциональность операционной системы. С++ не является операционной системой или платформой. И поэтому он инкапсулирует существующую функциональность операционных систем, таких как linux, unix и windows.
Однако pthreads также имеет хорошее обоснование для этого поведения. Из базовых спецификаций Open Group:
Эффект заключается в том, что более чем один поток может вернуться от своего вызова к pthread_cond_wait() или pthread_cond_timedwait() в результате одного вызовите pthread_cond_signal(). Этот эффект называется "ложным" пробуждение ". Обратите внимание, что ситуация самокорректируется тем, что число потоков, которые так пробуждены, конечно; например, следующий поток для вызова pthread_cond_wait() после последовательности событий выше блоки.
Хотя эта проблема может быть решена, потеря эффективности для которое возникает редко, является неприемлемым, особенно учитывая, что нужно проверить предикат, связанный с условием переменная в любом случае. Исправление этой проблемы излишне сократило бы степень concurrency в этом базовом строительном блоке для всех операции синхронизации более высокого уровня.
Дополнительное преимущество разрешения побочных пробуждений заключается в том, что приложения принудительно закодировать цикл предикат-тестирование вокруг условия ожидания. Это также заставляет приложение переносить лишнее состояние трансляции или сигналы на той же переменной условия, которая может быть закодирована в какой-то другой части приложения. Результирующие приложения таким образом, более надежным. Поэтому IEEE Std 1003.1-2001 явно документирует что могут возникнуть побочные пробуждения.
Таким образом, в основном утверждение состоит в том, что вы можете легко построить my_better_condition_variable
поверх переменной условия pthreads (или std::condition_variable
) довольно легко и без ущерба для производительности. Однако, если мы разместим my_better_condition_variable
на базовом уровне, то тем клиентам, которым не нужны функции my_better_condition_variable
, все равно придется заплатить за них.
Эта философия поместить самый быстрый, самый примитивный дизайн в нижней части стека, с намерением, чтобы над ним можно было создавать более лучшие/медленные вещи, работает по всей С++ lib. И где С++ lib не соблюдает эту философию, клиенты часто (и справедливо) раздражаются.
Ответ 2
Если вы не хотите писать цикл, вы можете использовать перегрузку которая вместо этого использует предикат:
cv.wait(lock, [&q]{ return !q.is_empty(); });
Он определен как эквивалентный циклу, поэтому он работает так же, как и исходный код.
Ответ 3
Даже если вы можете это сделать, спецификация С++ 11 позволяет cv.wait()
разблокировать ложно (для учета платформ, которые имеют такое поведение). Таким образом, даже если нет потоков стервятников (исключая аргумент о том, должны ли они существовать), потребительский поток не может ожидать, что там будет ждать куки файл и все еще нужно проверить.
Ответ 4
Я думаю, что это не безопасно:
void producer_thread() {
std::unique_lock<std::mutex> lock(m);
q.push(42);
cv.notify_one();
}
Вы все еще удерживаете блокировку, когда вы уведомляете другой поток, ожидающий блокировки. Таким образом, может случиться так, что другой поток сразу просыпается и пытается получить блокировку до того, как деструктор, вызванный после cv.notify_one()
, освободит блокировку. Это означает, что другой поток возвращается, чтобы ждать в конце концов навсегда.
Поэтому я думаю, что это должно быть закодировано как:
void producer_thread() {
std::unique_lock<std::mutex> lock(m);
q.push(42);
lock.unlock();
cv.notify_one();
}
или если вам не нравится разблокировать вручную как
void producer_thread() {
{
std::unique_lock<std::mutex> lock(m);
q.push(42);
}
cv.notify_one();
}