Рекурсивная блокировка (мьютекс) против Нерекурсивной блокировки (мьютекс)


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

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

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

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

6 164

6 ответов:

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

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

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

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

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

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

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

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

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

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

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

Как написал сам Дэйв Butenhof:

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

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

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

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

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

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

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

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

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