POSIX позволяет мьютексам быть рекурсивными. Это означает, что один и тот же поток может заблокировать один и тот же мьютекс дважды и не будет блокироваться. Конечно, он также должен разблокировать его дважды, иначе никакой другой поток не сможет получить мьютекс. Не все системы, поддерживающие потоки pthread, также поддерживают рекурсивные мьютексы, но если они хотят соответствовать POSIX, они должны это сделать .
Другие API (API более высокого уровня) также обычно предлагают мьютексы, часто называемые блокировками. Некоторые системы / языки (например, Cocoa Objective-C) предлагают как рекурсивные, так и нерекурсивные мьютексы. Некоторые языки также предлагают только один или другой. Например, в Java мьютексы всегда рекурсивны (один и тот же поток может дважды «синхронизироваться» с одним и тем же объектом). В зависимости от того, какие другие функциональные возможности потоков они предлагают, отсутствие рекурсивных мьютексов может быть проблемой, поскольку их можно легко написать самостоятельно (я сам уже реализовал рекурсивные мьютексы на основе более простых операций мьютексов / условий).
Чего я действительно не понимаю: для чего нужны нерекурсивные мьютексы? Зачем мне нужна тупиковая ситуация, если один и тот же мьютекс блокируется дважды? Даже языки высокого уровня, которые могли бы этого избежать (например, тестирование, если это приведет к взаимной блокировке, и выброс исключения, если это произойдет) обычно этого не делают. Вместо этого они позволят потоку заблокироваться.
Разве это только для случаев, когда я случайно блокирую его дважды и разблокирую только один раз, а в случае рекурсивного мьютекса будет сложнее найти проблему, поэтому вместо этого у меня сразу возникает тупик, чтобы увидеть, где появляется неправильная блокировка? Но разве я не могу сделать то же самое с возвращением счетчика блокировок при разблокировке и в ситуации, когда я уверен, что снял последнюю блокировку, а счетчик не равен нулю, я могу создать исключение или зарегистрировать проблему? Или есть какой-либо другой, более полезный вариант использования нерекурсивных мьютексов, который я не вижу? Или это может быть просто производительность, поскольку нерекурсивный мьютекс может быть немного быстрее, чем рекурсивный? Однако я проверил это, и разница действительно не так велика.
Ответ не в эффективности. Мьютексы без повторного входа приводят к лучшему коду.
Пример: A :: foo () получает блокировку. Затем он вызывает B :: bar (). Это нормально работало, когда вы это писали. Но через некоторое время кто-то изменяет B :: bar () на вызов A :: baz (), который также получает блокировку.
Что ж, если у вас нет рекурсивных мьютексов, это взаимоблокируется. Если они у вас есть, он работает, но может сломаться. A :: foo () мог оставить объект в несогласованном состоянии перед вызовом bar (), исходя из предположения, что baz () не может быть запущен, потому что он также получает мьютекс. Но, наверное, не должно работать! Человек, написавший A :: foo (), предположил, что никто не может вызвать A :: baz () одновременно - это и есть причина того, что оба этих метода получили блокировку.
Правильная ментальная модель использования мьютексов: мьютекс защищает инвариант. Когда мьютекс удерживается, инвариант может измениться, но перед освобождением мьютекса инвариант восстанавливается. Повторные блокировки опасны, потому что во второй раз, когда вы получаете блокировку, вы больше не можете быть уверены, что инвариант верен.
Если вас устраивают повторные блокировки, то это только потому, что вам не приходилось отлаживать подобную проблему раньше. Между прочим, в Java сейчас есть нереентерабельные блокировки в java.util.concurrent.locks.
источник
Semaphore
.A::foo()
возможно, объект все еще оставался в несогласованном состоянии перед вызовомA::bar()
. Какое отношение имеет мьютекс, рекурсивный или нет, к этому случаю?Как написал сам Дэйв Бутенхоф :
«Самая большая из всех больших проблем с рекурсивными мьютексами заключается в том, что они побуждают вас полностью потерять контроль над схемой блокировки и областью действия. Это смертельно. Зло. Это« пожиратель потоков ». Вы удерживаете блокировки в течение максимально короткого времени. Период. Всегда. Если вы вызываете что-то с заблокированной блокировкой просто потому, что не знаете, что она удерживается, или потому что вы не знаете, нужен ли вызываемый мьютекс, значит, вы удерживаете его слишком долго. нацелить дробовик на ваше приложение и нажать на курок. Предположительно, вы начали использовать потоки для обеспечения параллелизма, но вы просто ПРЕДОТВРАЩАЛИ параллелизм ».
источник
...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
Почему вы уверены, что это действительно правильная ментальная модель использования мьютексов? Я думаю, что правильная модель защищает данные, а не инварианты.
Проблема защиты инвариантов присутствует даже в однопоточных приложениях и не имеет ничего общего с многопоточностью и мьютексами.
Более того, если вам нужно защитить инварианты, вы все равно можете использовать двоичный семафор, который никогда не является рекурсивным.
источник
Одна из основных причин, по которой рекурсивные мьютексы полезны, - это многократный доступ к методам одним и тем же потоком. Например, скажем, если блокировка мьютекса защищает банк A / c от снятия, то если с этим снятием также связана комиссия, то должен использоваться тот же мьютекс.
источник
Единственный хороший вариант использования мьютекса рекурсии - это когда объект содержит несколько методов. Когда какой-либо из методов изменяет содержимое объекта и, следовательно, должен заблокировать объект, прежде чем состояние снова станет согласованным.
Если методы используют другие методы (например: addNewArray () вызывает addNewPoint () и завершает с помощью recheckBounds ()), но любая из этих функций сама по себе должна блокировать мьютекс, тогда рекурсивный мьютекс является беспроигрышным.
В любом другом случае (решать просто плохую кодировку, использовать ее даже в разных объектах) явно неправильно!
источник
Они абсолютно хороши, когда вам нужно убедиться, что мьютекс разблокирован, прежде чем что-то делать. Это потому, что
pthread_mutex_unlock
может гарантировать, что мьютекс разблокирован, только если он не рекурсивен.Если
g_mutex
нерекурсивный, приведенный выше код гарантированно будет вызыватьbar()
с разблокированным мьютексом. .Таким образом исключена возможность возникновения тупика в случае
bar()
, если неизвестная внешняя функция может сделать что-то, что может привести к тому, что другой поток попытается получить тот же мьютекс. Такие сценарии не редкость в приложениях, построенных на пулах потоков, и в распределенных приложениях, где межпроцессный вызов может порождать новый поток, даже не осознавая этого клиентским программистом. Во всех таких сценариях лучше всего вызывать указанные внешние функции только после снятия блокировки.Если бы он
g_mutex
был рекурсивным, просто не было бы возможности убедиться, что он разблокирован, прежде чем делать вызов.источник
class foo { ensureContains(item); hasItem(item); addItem(); }
еслиensureContains()
используетсяhasItem()
иaddItem()
, ваша разблокировка перед вызовом кого-то еще может предотвратить автоматическую взаимоблокировку, но также не позволяет ее правильному выполнению при наличии нескольких потоков. Как будто вы вообще не блокировали.ИМХО, большинство аргументов против рекурсивных блокировок (которые я использую 99,9% времени, например, за 20 лет параллельного программирования) смешивают вопрос о том, хороши они или плохи, с другими проблемами проектирования программного обеспечения, которые совершенно не связаны. Назовем одну из них, проблему «обратного вызова», которая исчерпывающе разработана без какой-либо точки зрения, связанной с многопоточностью, например, в книге « Программное обеспечение компонентов - за пределами объектно-ориентированного программирования». .
Как только у вас есть некоторая инверсия управления (например, запуск событий), вы сталкиваетесь с проблемами повторного входа. Независимо от того, задействованы ли мьютексы и потоки.
class EvilFoo { std::vector<std::string> data; std::vector<std::function<void(EvilFoo&)> > changedEventHandlers; public: size_t registerChangedHandler( std::function<void(EvilFoo&)> handler) { // ... } void unregisterChangedHandler(size_t handlerId) { // ... } void fireChangedEvent() { // bad bad, even evil idea! for( auto& handler : changedEventHandlers ) { handler(*this); } } void AddItem(const std::string& item) { data.push_back(item); fireChangedEvent(); } };
Теперь с помощью кода, подобного приведенному выше, вы получаете все случаи ошибок, которые обычно называются в контексте рекурсивных блокировок - только без каких-либо из них. Обработчик событий может отменить регистрацию после своего вызова, что приведет к ошибке в наивно написанном
fireChangedEvent()
. Или он может вызывать другие функции-члены,EvilFoo
вызывающие всевозможные проблемы. Основная причина - повторный вход. Хуже всего то, что это может быть даже не очень очевидным, поскольку это может происходить по целой цепочке событий, запускающих события, и в конечном итоге мы возвращаемся к нашему EvilFoo (нелокальному).Итак, основная проблема - это повторный вход, а не рекурсивная блокировка. Теперь, если вы чувствовали себя в большей безопасности, используя нерекурсивную блокировку, как могла бы проявиться такая ошибка? В тупике всякий раз, когда происходит неожиданный повторный вход. А с рекурсивной блокировкой? Точно так же это проявится в коде без каких-либо блокировок.
Таким образом, злой стороной
EvilFoo
являются события и то, как они реализованы, а не рекурсивная блокировка.fireChangedEvent()
сначала нужно будет создать копиюchangedEventHandlers
и использовать ее для итерации.Еще один часто обсуждаемый аспект - это определение того, что блокировка должна делать в первую очередь:
Как я занимаюсь параллельным программированием, у меня есть ментальная модель последнего (защита ресурса). Это основная причина, по которой я хорошо разбираюсь в рекурсивных блокировках. Если какая-либо функция (член) требует блокировки ресурса, она блокируется. Если он вызывает другую функцию (член) во время выполнения своих действий, и эта функция также требует блокировки - она блокируется. И мне не нужен «альтернативный подход», потому что подсчет ссылок рекурсивной блокировки такой же, как если бы каждая функция написала что-то вроде:
void EvilFoo::bar() { auto_lock lock(this); // this->lock_holder = this->lock_if_not_already_locked_by_same_thread()) // do what we gotta do // ~auto_lock() { if (lock_holder) unlock() } }
И как только в игру войдут события или подобные конструкции (посетители ?!), я не надеюсь, что все последующие проблемы дизайна будут решены какой-нибудь нерекурсивной блокировкой.
источник