Рекурсивная блокировка (мьютекс) против Нерекурсивной блокировки (мьютекс)
POSIX позволяет мьютексам быть рекурсивными. Это означает, что один и тот же поток может заблокировать один и тот же мьютекс дважды и не будет взаимоблокировкой. Конечно, он также должен разблокировать его дважды, иначе никакой другой поток не сможет получить мьютекс. Не все системы, поддерживающие pthreads, также поддерживают рекурсивные мьютексы, но если они хотят быть POSIX соответствуют, они должны.
другие API (более высокого уровня API) также обычно предлагают мьютексы, часто называемые блокировками. Некоторые системы / языки (например, Cocoa Objective-C) предлагают как рекурсивные, так и нерекурсивные мьютексы. Некоторые языки предлагают только один или другой. Е. Г. в Java мьютексы всегда рекурсивный (один и тот же поток может дважды "синхронизировать" на один и тот же объект). В зависимости от того, какие другие функции потока они предлагают, отсутствие рекурсивных мьютексов не может быть проблемой, так как они могут быть легко написаны самостоятельно (я уже реализовал рекурсивные мьютексы на основе более простых операций мьютекса/условия).
Что Я не совсем понимаю: для чего нужны нерекурсивные мьютексы? Почему я хочу иметь тупик потока, если он блокирует один и тот же мьютекс дважды? Даже языки высокого уровня, которые могут этого избежать (например, тестирование, если это приведет к взаимоблокировке и вызовет исключение, если это произойдет), обычно этого не делают. Вместо этого они позволят потоку взаимоблокироваться.
Это только для случаев, когда я случайно блокирую его дважды и открываю только один раз, и в случае рекурсивного мьютекса было бы сложнее найти проблема, поэтому вместо этого у меня есть тупик сразу, чтобы увидеть, где появляется неправильная блокировка? Но не могу ли я сделать то же самое с возвратом счетчика блокировки при разблокировке и в ситуации, когда я уверен, что освободил последний замок, а счетчик не равен нулю, я могу создать исключение или зарегистрировать проблему? Или есть ли другой, более полезный вариант использования нерекурсивных мьютексов, который я не вижу? Или это может быть просто производительность, так как нерекурсивный мьютекс может быть немного быстрее рекурсивного? Тем не менее, я проверил это, и разница действительно не такая большая.
6 ответов
разница между рекурсивным и нерекурсивным мьютексом связана с владением. В случае рекурсивного мьютекса ядро должно отслеживать поток, который фактически получил мьютекс в первый раз, чтобы он мог обнаружить разницу между рекурсией и другим потоком, который должен блокировать вместо этого. Как указал другой ответ, возникает вопрос о дополнительных накладных расходах на это как с точки зрения памяти для хранения этого контекста, так и циклов, необходимых для поддерживаю его.
есть и другие соображения играют здесь тоже.
поскольку рекурсивный мьютекс имеет чувство собственности, поток, который захватывает мьютекс, должен быть тем же потоком, который освобождает мьютекс. В случае нерекурсивных мьютексов нет чувства собственности, и любой поток обычно может освободить мьютекс независимо от того, какой поток изначально взял мьютекс. Во многих случаях этот тип "мьютекса" действительно больше похож на семафорное действие, если мьютекс не обязательно используется в качестве устройства исключения, а используется как устройство синхронизации или сигнализации между двумя или более потоками.
другое свойство, которое поставляется с чувством собственности в мьютексе, - это способность поддерживать наследование приоритета. Поскольку ядро может отслеживать поток, владеющий мьютексом, а также идентификацию всех блокировщиков, в системе с приоритетным потоком становится возможным повысить приоритет потока, которому в настоящее время принадлежит мьютекс к приоритету потока с наивысшим приоритетом, который в настоящее время блокируется на мьютексе. Это наследование предотвращает проблему инверсии приоритета, которая может возникнуть в таких случаях. (Обратите внимание, что не все системы поддерживают наследование приоритета на таких мьютексах, но это еще одна функция, которая становится возможной через понятие собственности).
Если вы ссылаетесь на классическое ядро VxWorks RTOS, они определяют три механизма:
- мьютекс - поддерживает рекурсию, и необязательно приоритет наследования
- двоичный семафор - нет рекурсии, нет наследования, простое исключение, берущий и дающий не должны быть одинаковыми потоками, доступен широковещательный релиз
- счетный семафор - нет рекурсии или наследования, действует как когерентный счетчик ресурсов из любого желаемого начального счета, потоки только блок, где чистый счет ресурса равен нулю.
опять же, это несколько зависит от платформа-особенно то, что они называют этими вещами, но это должны быть репрезентативные концепции и различные механизмы в игре.
ответ не эффективность. Нереентерабельные мьютексы по улучшению кода.
пример: A:: foo() получает блокировку. Затем он вызывает B:: bar(). Это сработало отлично, когда вы написали это. Но через некоторое время кто-то меняет B::bar() на вызов A::baz(), который также получает блокировку.
Ну, если у вас нет рекурсивных мьютексов, это тупики. Если они у вас есть, он бежит, но может сломаться. A:: foo () возможно, оставил объект в несогласованном состоянии перед вызовом bar (), исходя из предположения, что baz () не может быть запущен, потому что он также получает мьютекс. Но он, вероятно, не должен бежать! Человек, написавший A:: foo() предположил, что никто не может вызвать A:: baz() одновременно - вот и вся причина, по которой оба этих метода приобрели блокировку.
правильная ментальная модель для использования мьютексов: мьютекс защищает инвариант. Когда мьютекс удерживается, инвариант может измениться, но перед освобождением мьютекса инвариант восстановлен. Повторные блокировки опасны, потому что во второй раз вы не можете быть уверены, что инвариант истинен.
Если вы довольны блокировками reentrant, это только потому, что вам не приходилось отлаживать такую проблему раньше. Java имеет нереентерабельные замки в эти дни в Java.утиль.параллельный.замки, кстати.
Как написал сам Дэйв Butenhof:
" самая большая из всех больших проблем с рекурсивными мьютексами заключается в том, что они ободряют вас совершенно потерять след вашей фиксируя схемы и масштаб. Это смертельно. Зло. Это "пожиратель нитей". Вы держите замки для абсолютно кратчайшие сроки. Период. Всегда. Если вы звоните что-то с замком удерживается просто потому, что вы не знаете, что он удерживается, или потому что вы не знаете, нужен ли вызываемому мьютекс, тогда ты слишком долго. Вы нацеливаете дробовик на ваше заявление и нажать на курок. Предположительно, вы начали использовать потоки для получения параллелизм; но вы только что предотвратили параллелизм."
правильная ментальная модель для использования мьютексы: мьютекс защищает инвариантный.
почему вы уверены, что это действительно правильная ментальная модель для использования мьютексов? Я думаю, что правильная модель защищает данные, но не инварианты.
проблема защиты инвариантов присутствует даже в однопоточных приложениях и не имеет ничего общего с многопоточностью и мьютексами.
кроме того, если вам нужно защитить инварианты, вы все равно можете используйте двоичный семафор, который никогда не рекурсивен.
одна из основных причин того, что рекурсивные мьютексы полезны в случае доступа к методам несколько раз одним и тем же потоком. Например, скажем, если блокировка мьютекса защищает банковский A / c для вывода, то если есть плата, также связанная с этим выводом, то должен использоваться тот же мьютекс.
единственный хороший вариант использования рекурсивного мьютекса - это когда объект содержит несколько методов. Когда какой-либо из методов изменяет содержимое объекта и поэтому должен заблокировать объект, прежде чем состояние снова будет согласованным.
Если методы используют другие методы (т. е.: addNewArray () вызывает addNewPoint () и завершает с помощью recheckBounds ()), но любая из этих функций сама по себе должна заблокировать мьютекс, то рекурсивный мьютекс является беспроигрышным.
для любого другого случая (решение просто плохого кодирования, используя его даже в разных объектах) явно неправильно!