Рекурсивная блокировка (мьютекс) против нерекурсивной блокировки (мьютекс) - PullRequest
169 голосов
/ 09 октября 2008

POSIX позволяет мьютексам быть рекурсивными. Это означает, что один и тот же поток может блокировать один и тот же мьютекс дважды и не блокируется. Конечно, он также должен разблокировать его дважды, иначе никакой другой поток не сможет получить мьютекс. Не все системы, поддерживающие pthreads, также поддерживают рекурсивные мьютексы, но если они хотят соответствовать POSIX, они должны .

Другие API (более высокоуровневые API) также обычно предлагают мьютексы, часто называемые Locks. Некоторые системы / языки (например, Cocoa Objective-C) предлагают как рекурсивные, так и нерекурсивные мьютексы. Некоторые языки также предлагают только один или другой. Например. в Java мьютексы всегда рекурсивны (один и тот же поток может дважды «синхронизироваться» с одним и тем же объектом). В зависимости от того, какую другую функциональность потоков они предлагают, отсутствие рекурсивных мьютексов может быть проблемой, поскольку их можно легко написать самостоятельно (я уже сам реализовал рекурсивные мьютексы на основе более простых операций мьютекс / условие).

Что я не очень понимаю: для чего нужны нерекурсивные мьютексы? Зачем мне нужен тупик потока, если он дважды блокирует один и тот же мьютекс? Даже языки высокого уровня, которые могут этого избежать (например, тестирование, если это приведет к взаимоблокировке, и создание исключения, если это произойдет), обычно этого не делают. Вместо этого они позволят заблокировать поток.

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

Ответы [ 6 ]

141 голосов
/ 10 октября 2008

Разница между рекурсивным и нерекурсивным мьютексом связана с владением. В случае рекурсивного мьютекса ядро ​​должно отслеживать поток, который фактически получил мьютекс в первый раз, чтобы он мог обнаружить разницу между рекурсией и другим потоком, который должен вместо этого блокироваться. Как указал другой ответ, возникает вопрос о дополнительных затратах на это как с точки зрения памяти для хранения этого контекста, так и с точки зрения циклов, необходимых для его поддержания.

Однако , здесь есть и другие соображения.

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

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

Если вы ссылаетесь на классическое ядро ​​VOSWorks RTOS, они определяют три механизма:

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

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

116 голосов
/ 16 ноября 2008

Ответ не эффективность. Не возвращающиеся взаимные исключения приводят к лучшему коду.

Пример: A :: foo () получает блокировку. Затем он вызывает B :: bar (). Это работало нормально, когда ты это написал. Но через некоторое время кто-то меняет B :: bar () на вызов A :: baz (), который также получает блокировку.

Ну, если у вас нет рекурсивных мьютексов, это тупики. Если они у вас есть, они запускаются, но могут сломаться. A :: foo (), возможно, оставил объект в несогласованном состоянии перед вызовом bar (), предполагая, что baz () не может быть запущен, потому что он также получает мьютекс. Но это, вероятно, не должно бежать! Тот, кто написал A :: foo (), предположил, что никто не может вызвать A :: baz () одновременно - вот и вся причина того, что оба этих метода получили блокировку.

Правильная ментальная модель для использования мьютексов: мьютекс защищает инвариант. Когда мьютекс удерживается, инвариант может измениться, но перед освобождением мьютекса инвариант восстанавливается. Повторно входящие блокировки опасны, потому что во второй раз, когда вы получаете блокировку, вы не можете быть уверены, что инвариант больше верен.

Если вы довольны повторяющимися блокировками, это только потому, что вам раньше не приходилось отлаживать подобную проблему. Между прочим, в Java есть не реентерабельные блокировки в java.util.concurrent.locks.

85 голосов
/ 07 августа 2009

Как написал сам Дэйв Бутенхоф :

"Самая большая из всех больших проблем с рекурсивными мьютексами заключается в том, что они призывают вас полностью потерять свою схему блокировки и объем. Это смертельно. Злой. Это «пожиратель ниток». Вы держите замки для абсолютно кратчайшее время. Период. Всегда. Если ты звонишь что-то с замком, удерживаемым просто потому, что вы не знаете, оно удерживается потому что вы не знаете, нужен ли вызываемый мьютекс, то вы держа его слишком долго Вы нацеливаете дробовик на ваше приложение и нажать на курок. Вы, вероятно, начали использовать потоки, чтобы получить параллелизм; но вы только что предотвратили параллелизм. "

13 голосов
/ 16 декабря 2008

Правильная ментальная модель для использования мьютексы: мьютекс защищает инвариантно.

Почему вы уверены, что это действительно правильная ментальная модель для использования мьютексов? Я думаю, что правильная модель защищает данные, но не инварианты.

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

Кроме того, если вам нужно защитить инварианты, вы все равно можете использовать двоичный семафор, который никогда не бывает рекурсивным.

4 голосов
/ 29 мая 2015

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

3 голосов
/ 20 ноября 2015

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

Если методы используют другие методы (например, addNewArray () вызывает addNewPoint () и завершается с помощью recheckBounds ()), но любая из этих функций сама по себе должна блокировать мьютекс, то рекурсивный мьютекс является беспроигрышным.

Для любого другого случая (решение просто плохого кодирования, использование его даже в разных объектах) явно неправильно!

...