Не рекурсивное владение мьютексом
Я прочитал этот ответ на SO:
Поскольку рекурсивный мьютекс имеет чувство собственности, поток, который захватывает мьютекс, должен быть тем же потоком, который освобождает мьютекс. В случае нерекурсивных мьютексов нет смысла владения, и любой поток обычно может освободить мьютекс независимо от того, какой поток первоначально захватил мьютекс.
Я смущен последним утверждением. Может ли один поток заблокировать мьютекс, а другой - разблокировать этот мьютекс? Я думал, что та же самая нить должна быть единственной, способной разблокировать мьютекс? Или есть какой-то конкретный мьютекс, который позволяет это? Я надеюсь, что кто-то может уточнить.
3 ответа
Нерекурсивный мьютекс
Большинство мьютексов (или, по крайней мере, должны) быть нерекурсивными. Мьютекс - это объект, который может быть получен или выпущен атомарно, что позволяет защитить данные, которые совместно используются несколькими потоками, от состояния гонки, повреждения данных и других неприятных вещей.
Один мьютекс должен быть получен только один раз одним потоком в одной цепочке вызовов. Попытка получить (или удерживать) один и тот же мьютекс дважды в одном и том же контексте потока должна рассматриваться как недопустимый сценарий и должна обрабатываться надлежащим образом (обычно через ASSERT, поскольку вы нарушаете фундаментальный контракт вашего кода).
Рекурсивный мьютекс
Это следует считать запахом кода или взломом. Единственный способ, которым рекурсивный мьютекс отличается от стандартного мьютекса, состоит в том, что рекурсивный мьютекс может быть получен несколько раз одним и тем же потоком.
Основной причиной необходимости рекурсивного мьютекса является отсутствие владения и отсутствие четкой цели или разграничения между классами. Например, ваш код может вызывать другой класс, который затем вызывает ваш класс. Затем начальный класс может попытаться снова получить тот же мьютекс, и, поскольку вы хотите избежать сбоя, вы реализуете его как рекурсивный мьютекс.
Такого рода иерархия классов с переворотом может привести к всевозможным головным болям, а рекурсивный мьютекс обеспечивает только решение проблемы с бинтами для решения более фундаментальной архитектурной проблемы.
Независимо от типа мьютекса, он всегда должен быть одним и тем же потоком, который получает и освобождает один и тот же мьютекс. Общий шаблон, который вы используете в коде, выглядит примерно так:
Thread 1
Acquire mutex A
// Modify or read shared data
Release mutex A
Thread 2
Attempt to acquire mutex A
Block as thread 1 has mutex A
When thread 1 has released mutex A, acquire it
// Modify or read shared data
Release mutex A
Это становится сложнее, когда у вас есть несколько мьютексов, которые могут быть получены одновременно (скажем, мьютексы A и B). Существует риск того, что вы попадете в тупиковую ситуацию, подобную этой:
Thread 1
Acquire mutex A
// Access some data...
*** Context switch to thread 2 ***
Thread 2
Acquire mutex B
// Access some data
*** Context switch to thread 1 ***
Attempt to acquire mutex B
Wait for thread 2 to release mutex B
*** Context switch to thread 2 ***
Attempt to acquire mutex A
Wait for thread 1 to release mutex A
*** DEADLOCK ***
Теперь у нас есть ситуация, когда каждый поток ожидает, пока другой поток освободит другую блокировку - это называется шаблоном взаимоблокировки ABBA.
Чтобы предотвратить эту ситуацию, важно, чтобы каждый поток всегда получал мьютексы в одном и том же порядке (например, всегда A, затем B).
Я думаю, что это охватывает все ваши вопросы. Прямо из man-страниц Linux для pthreads:
Если тип мьютекса - PTHREAD_MUTEX_NORMAL, обнаружение взаимоблокировки не должно предоставляться. Попытка разблокировать мьютекс вызывает тупик. Если поток пытается разблокировать мьютекс, который он не заблокировал, или мьютекс, который разблокирован, возникает неопределенное поведение.
Если тип мьютекса - PTHREAD_MUTEX_ERRORCHECK, то должна быть обеспечена проверка ошибок. Если поток пытается заблокировать мьютекс, который он уже заблокировал, должна быть возвращена ошибка. Если поток пытается разблокировать мьютекс, который он не заблокировал, или мьютекс, который разблокирован, должна быть возвращена ошибка.
Если тип мьютекса - PTHREAD_MUTEX_RECURSIVE, то мьютекс должен поддерживать концепцию счетчика блокировок. Когда поток успешно получает мьютекс в первый раз, счетчик блокировок должен быть установлен в единицу. Каждый раз, когда поток повторно блокирует этот мьютекс, счетчик блокировок должен увеличиваться на единицу. Каждый раз, когда поток разблокирует мьютекс, количество блокировок должно уменьшаться на единицу. Когда количество блокировок достигает нуля, мьютекс должен стать доступным для других потоков. Если поток пытается разблокировать мьютекс, который он не заблокировал, или мьютекс, который разблокирован, должна быть возвращена ошибка.
Если тип мьютекса - PTHREAD_MUTEX_DEFAULT, попытка рекурсивной блокировки мьютекса приводит к неопределенному поведению. Попытка разблокировать мьютекс, если он не был заблокирован вызывающим потоком, приводит к неопределенному поведению. Попытка разблокировать мьютекс, если он не заблокирован, приводит к неопределенному поведению.
Рекурсивные мьютексы по своей структуре специфичны для потоков (одна и та же блокировка потока снова = рекурсия, другая блокировка потока между тем = блокировка). Обычные мьютексы не имеют такой конструкции, и, таким образом, они могут быть заблокированы и разблокированы в разных потоках.