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

multithreading locking mutex deadlock recursive-mutex

108553 просмотра

6 ответа

81990 Репутация автора

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

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

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

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

Автор: Mecki Источник Размещён: 09.10.2008 03:19

Ответы (6)


142 плюса

8300 Репутация автора

Решение

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

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

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

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

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

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

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

Автор: Tall Jeff Размещён: 10.10.2008 01:09

116 плюса

1806 Репутация автора

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

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

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

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

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

Автор: Jonathan Размещён: 16.11.2008 07:44

13 плюса

0 Репутация автора

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

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

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

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

Автор: Corpse Размещён: 16.12.2008 11:41

85 плюса

3367 Репутация автора

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

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

Автор: Chris Cleeland Размещён: 07.08.2009 02:20

4 плюса

41 Репутация автора

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

Автор: avis Размещён: 29.05.2015 01:41

3 плюса

7382 Репутация автора

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

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

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

Автор: DarkZeros Размещён: 20.11.2015 02:30
32x32