Почему pthread_cond_wait имеет ложные пробуждения?

145

Чтобы процитировать man страницу:

При использовании условных переменных всегда существует логический предикат, включающий общие переменные, связанные с каждым условным ожиданием, которое истинно, если поток должен продолжить. Могут возникнуть ложные пробуждения от функций pthread_cond_timedwait () или pthread_cond_wait (). Поскольку возврат из pthread_cond_timedwait () или pthread_cond_wait () ничего не подразумевает в значении этого предиката, предикат должен быть переоценен после такого возврата.

Таким образом, pthread_cond_waitможете вернуться, даже если вы не сообщили об этом. По крайней мере, на первый взгляд это кажется довольно жестоким. Это было бы похоже на функцию, которая случайно вернула неправильное значение или вернула случайно, прежде чем она действительно достигла правильного оператора возврата. Это похоже на серьезную ошибку. Но тот факт, что они решили документировать это на странице руководства, а не исправлять это, похоже, указывает на то, что есть законная причина, по которой мы pthread_cond_waitвнезапно просыпаемся. Предположительно, есть что-то внутреннее в том, как это работает, что делает его таким, что с этим ничего не поделаешь. Вопрос в том, что.

Почему же pthread_cond_waitвернуться поддельно? Почему он не может гарантировать, что проснется только тогда, когда на него правильно подали сигналы? Кто-нибудь может объяснить причину его ложного поведения?

Джонатан М Дэвис
источник
5
Я предполагаю, что это связано с возвратом, когда процесс ловит сигнал. Большинство * nixes не перезапускают блокирующий вызов после того, как сигнал прерывает его; они просто устанавливают / возвращают код ошибки, который сообщает, что произошел сигнал.
cHao
1
@cHao: хотя обратите внимание, что, поскольку переменные условия в любом случае имеют другие причины для ложных пробуждений, обработка сигнала не является ошибкой для pthread_cond_(timed)wait: «Если сигнал доставлен ... поток возобновляет ожидание переменной условия, как если бы он был не прервано, или оно должно вернуть ноль из-за ложного пробуждения ". Другие функции блокировки указывают, EINTRкогда прервано сигналом (например read) или требуется возобновить (например pthread_mutex_lock). Так что, если бы не было других причин для ложного пробуждения, pthread_cond_waitможно было бы определить, как любой из них.
Стив Джессоп
4
Связанная статья в Википедии:
Ложное
Многие функции не могут полностью выполнить свою работу полностью (прерванный ввод / вывод), и функции наблюдения могут получать не событие, как изменение каталога, в котором изменение было отменено или возвращено обратно. В чем проблема?
любопытный парень

Ответы:

77

Следующее объяснение дано Дэвидом Р. Бутенхофом в «Программирование с помощью потоков POSIX» (стр. 80):

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

В следующем обсуждении comp.programming.threads он расширяет концепцию дизайна:

Патрик Дойл написал: 
> В статье Том Пейн писал: 
>> Каз Кылхеку писал: 
>>: Это так, потому что реализации иногда не могут избежать вставки 
>>: эти ложные пробуждения; это может быть дорогостоящим, чтобы предотвратить их.

>> Но почему? Почему это так сложно? Например, мы говорим о
>> ситуации, когда время ожидания истекает, когда поступает сигнал? 

> Вы знаете, мне интересно, использовали ли разработчики pthreads такую ​​логику: 
> пользователи условных переменных в любом случае должны проверять условие при выходе, 
> поэтому мы не будем возлагать на них никакого дополнительного бремени, если позволим 
> ложные пробуждения; и так как это возможно, что позволяет ложным
> пробуждения могут сделать реализацию быстрее, это может помочь, только если мы 
> Позволь им. 

> Возможно, они не имели в виду какую-то конкретную реализацию. 

Вы на самом деле совсем недалеко, за исключением того, что вы не продвинулись достаточно далеко. 

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

Мы следовали этому намерению с несколькими уровнями оправдания. Первое было
«религиозное» использование цикла защищает приложение от собственного несовершенства 
практика кодирования. Во-вторых, абстрактно представить было несложно.
машины и код реализации, который может использовать это требование для улучшения 
выполнение операций ожидания среднего состояния за счет оптимизации 
механизмы синхронизации. 
/ ------------------ [David.Buten ... @ compaq.com] ------------------ \ 
| Компьютерная корпорация Compaq POSIX Thread Architect |
| Моя книга: http://www.awl.com/cseng/titles/0-201-63392-2/ |
\ ----- [http://home.earthlink.net/~anneart/family/dave.html] ----- / 

NPE
источник
22
в основном это ничего не говорит. Здесь не дается никакого объяснения, кроме первоначальной мысли, что «это может сделать вещи быстрее», но никто не знает, как это сделать, или вообще не знает.
Богдан Ионица
107

Есть по крайней мере две вещи, которые может означать «ложное пробуждение»:

  • Поток, заблокированный в, pthread_cond_waitможет вернуться из вызова, даже если не было выполнено ни одного вызова pthread_call_signalили pthread_cond_broadcastусловия.
  • Поток, заблокированный в pthread_cond_waitвозвратах, возвращается из-за вызова pthread_cond_signalили pthread_cond_broadcast, однако после повторного запроса мьютекса нижележащий предикат оказывается больше не истинным.

Но последний случай может иметь место, даже если реализация переменной условия не допускает первый случай. Рассмотрим очередь потребителя производителя и три потока.

  • Поток 1 только что удалил элемент из очереди и освободил мьютекс, и очередь теперь пуста. Поток делает все, что он делает с элементом, который он приобрел на некотором процессоре.
  • Поток 2 пытается удалить из очереди элемент, но обнаруживает, что очередь пуста при проверке под мьютексом, вызовами pthread_cond_waitи блоками в вызове, ожидающем сигнал / трансляцию.
  • Поток 3 получает мьютекс, вставляет новый элемент в очередь, уведомляет переменную условия и снимает блокировку.
  • В ответ на уведомление от потока 3 запланирован запуск потока 2, который ожидал выполнения условия.
  • Однако прежде чем потоку 2 удастся войти в ЦП и захватить блокировку очереди, поток 1 завершает свою текущую задачу и возвращается в очередь для дополнительной работы. Он получает блокировку очереди, проверяет предикат и обнаруживает, что в очереди есть работа. Он переходит в очередь на элемент, который был вставлен потоком 3, снимает блокировку и делает все, что делает с элементом, который был помещен в поток 3.
  • Поток 2 теперь попадает на ЦП и получает блокировку, но когда он проверяет предикат, он обнаруживает, что очередь пуста. Поток 1 «украл» предмет, поэтому пробуждение кажется ложным. Поток 2 должен снова ждать условия.

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

ACM
источник
23
да. По сути, это то, что происходит, когда вместо механизма синхронизации со счетчиком используется событие. К сожалению, кажется, что семафоры POSIX (в любом случае в Linux) также подвержены пробуждению spurius. Я просто нахожу немного странным, что сбой фундаментальной функциональности примитивов синхронизации просто воспринимается как «нормальный» и должен решаться на уровне пользователя :( Предположительно, разработчики будут противостоять, если системный вызов задокументирован с разделом «Ложное сегфо» или, возможно, «Ложное соединение с неправильным URL» или «Ложное открытие неправильного файла».
Мартин Джеймс
2
Более распространенный сценарий «ложного пробуждения», скорее всего, является побочным эффектом вызова pthread_cond_broadcast (). Допустим, у вас есть пул из 5 потоков, два просыпаются на трансляцию и выполняют работу. Остальные трое просыпаются и обнаруживают, что работа выполнена. Многопроцессорные системы могут также привести к условному сигналу, который случайно просыпается из нескольких потоков. Код просто проверяет предикат снова, видит недопустимое состояние и возвращается в спящий режим. В любом случае проверка предиката решает проблему. IMO, в общем, пользователи не должны использовать необработанные мьютексы POSIX и условные выражения.
CubicleSoft
1
@MartinJames - Как насчет классического «ложного» EINTR? Я согласен, что постоянное тестирование EINTR в цикле немного раздражает и делает код довольно уродливым, но разработчики все равно делают это, чтобы избежать случайных поломок.
CubicleSoft
2
@Yola Нет, не может, потому что вы должны заблокировать мьютекс вокруг, pthread_cond_signal/broadcastи вы не сможете сделать это, пока мьютекс не будет разблокирован вызовом pthread_cond_wait.
a3f
1
Пример этого ответа очень реалистичен, и я согласен, что проверка предикатов - хорошая идея. Тем не менее, это не может быть исправлено одинаково разумно, если выполнить проблемный шаг «поток 1 завершает свою текущую задачу и возвращается в очередь для дополнительной работы», а заменить его на «поток 1 завершить свою текущую задачу и вернуться к ожиданию на переменная условия "? Это исключит режим сбоя, описанный в ответе, и я почти уверен, что он сделает код корректным при отсутствии ложных пробуждений . Есть ли какая-нибудь реальная реализация, которая производит ложные пробуждения на практике?
Quuxplusone
7

Раздел «Многократное пробуждение по сигналу условия» в pthread_cond_signal содержит пример реализации pthread_cond_wait и pthread_cond_signal, который включает в себя ложные пробуждения.

Цзинго Яо
источник
2
Я думаю, что этот ответ неверен, насколько он идет. Пример реализации на этой странице имеет реализацию «notify one», которая эквивалентна «notify all»; но это, кажется, не производит фактически ложные пробуждения. Единственный способ для потока проснуться - это какой-то другой поток, вызывающий «уведомить все», или каким-то другим потоком, вызывающим «вещь, помеченная», «уведомить одного», которая действительно уведомляет всех.
Quuxplusone
5

Хотя я не думаю, что это учитывалось во время проектирования, здесь есть реальная техническая причина: в сочетании с отменой потока существуют условия, при которых использование режима пробуждения «внезапно» может быть абсолютно необходимым, по крайней мере, если вы готовы наложить очень-очень сильные ограничения на то, какие стратегии реализации возможны.

Ключевая проблема заключается в том, что, если поток действует на отмену, в то время как заблокирован pthread_cond_wait, побочные эффекты должны быть такими, как если бы он не потреблял никакого сигнала для переменной условия. Однако трудно (и весьма ограниченно) убедиться, что вы еще не использовали сигнал, когда начинаете действовать на отмену, и на этом этапе может оказаться невозможным «повторно отправить» сигнал в переменную условия, поскольку вы можете быть в ситуации, когда вызывающая pthread_cond_signalсторона уже имеет право уничтожить кондвара и освободить память, в которой он находился.

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

R .. GitHub ОСТАНОВИТЬ, ПОМОГАЯ ЛЕД
источник