multithreading - мьютексы - работа с мьютексами c++




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

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

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

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

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

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

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

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

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


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

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

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


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

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

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

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

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


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

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

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

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

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

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

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





recursive-mutex