Рекурсивная блокировка (Mutex) против нерекурсивной блокировки (Mutex)

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

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

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

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

Ответ 1

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

Ответ 3

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

"Самая большая из всех больших проблем с рекурсивными мьютексами - это то, что они рекомендуют вам полностью потерять контроль за вашей схемой блокировки и объем. Это смертельно. Зло. Это "пожиратель нитей". У вас есть блокировки для абсолютно кратчайшее время. Период. Всегда. Если вы звоните что-то с замком, проведенным просто потому, что вы не знаете, что оно удерживается, или потому что вы не знаете, нуждается ли вызов в мьютексе, тогда вы держа его слишком долго. Вы нацеливаете дробовик на свое приложение и потянув за курок. Вы предположительно начали использовать потоки, чтобы получить concurrency; но вы только ПРЕДОТВРАТЬ concurrency. "

Ответ 4

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

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

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

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

Ответ 5

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

Ответ 6

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

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

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