Почему я не могу проверить, заблокирован ли мьютекс?

28

В C ++ 14, похоже, пропущен механизм проверки, std::mutexзаблокирован ли объект или нет. Посмотрите этот ТАК вопрос:

/programming/21892934/how-to-assert-if-a-stdmutex-is-locked

Есть несколько способов обойти это, например, используя;

std::mutex::try_lock()
std::unique_lock::owns_lock()

Но ни один из них не является особенно удовлетворительным решением.

try_lock()разрешено возвращать ложный минус и имеет неопределенное поведение, если текущий поток заблокировал мьютекс. У этого также есть побочные эффекты. owns_lock()требует построения unique_lockповерх оригинала std::mutex.

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

Возможность проверить состояние мьютекса (например std::mutex::is_locked()) не кажется мне эзотерическим запросом, поэтому я подозреваю, что Комитет по стандартизации намеренно пропустил эту функцию, а не упустил ее.

Зачем?

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

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

шест для отталкивания
источник
42
Вы не можете разумно проверить, заблокирован ли мьютекс, потому что через одну наносекунду после проверки он может быть разблокирован или заблокирован. Поэтому, если вы написали «if (mutex_is_locked ()) ...», тогда mutex_is_locked может вернуть правильный результат, но к тому времени, когда будет выполнено «if», это неверно.
gnasher729
1
Это ^. Какую полезную информацию вы надеетесь получить is_locked?
бесполезно
3
Это похоже на проблему XY. Почему вы пытаетесь предотвратить повторное использование родителей только во время рождения ребенка? Есть ли у вас требование, чтобы у любого родителя могло быть только одно потомство? Ваш замок не помешает этому. У вас нет ясных поколений? Если нет, знаете ли вы, что люди, которые могут быть оптимизированы быстрее, имеют более высокую приспособленность, так как их можно выбирать чаще / раньше? Если вы используете поколения, почему бы вам не выбрать всех родителей заранее, а затем позволить потокам извлекать родителей из очереди? Действительно ли создание потомства настолько дорого, что вам нужно несколько потоков?
Амон
10
@quant - я не понимаю, почему мьютексы ваших родительских объектов в вашем примере приложения вообще должны быть мьютексами: если у вас есть мастер-мьютекс, который блокируется всякий раз, когда они установлены, вы можете просто использовать логическую переменную, чтобы указать их статус.
Периата Breatta
4
Я не согласен с последним предложением вопроса. Простое логическое значение здесь намного чище, чем мьютекс. Сделайте это атомарным булом, если вы не хотите блокировать мастер мьютекс для «возврата» родителя.
Себастьян Редл

Ответы:

53

Я вижу как минимум две серьезные проблемы с предложенной операцией.

Первый из них уже упоминался в комментарии @ gnasher729 :

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

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

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

Если я правильно понимаю, то обе проблемы были бы спорными в вашем конкретном случае благодаря блокировке мастер-мьютекса. Но это не кажется мне частым случаем, поэтому я думаю, что комитет поступил правильно, не добавив функцию, которая может быть несколько полезной в очень особых сценариях и наносить ущерб всем остальным. (В духе: «Сделайте интерфейсы простыми в использовании, правильными и трудными в использовании неправильно».)

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

5gon12eder
источник
Спасибо, это хороший ответ. Причина, по которой я не хочу поддерживать очередь готовых родителей, заключается в том, что мне нужно сохранить порядок, в котором были созданы родители (поскольку это определяет их продолжительность жизни). Это легко сделать с помощью очереди LIFO. Если я начну что-то ломать, мне придется поддерживать отдельный механизм упорядочивания, который усложнит вещи, отсюда и текущий подход.
кв.
14
@quant: Если у вас есть две цели поставить родителей в очередь, вы можете сделать это с двумя очередями ....
@quant: Вы удаляете элемент (самое большее) один раз, но, вероятно, выполняете обработку каждый раз несколько раз, поэтому вы оптимизируете редкий случай за счет общего случая. Это редко желательно.
Джерри Коффин
2
Но это разумно спросить , является ли текущий поток заблокирован мьютекс.
Ограниченное искупление
@LimitedAtonement Не совсем. Для этого мьютекс должен хранить дополнительную информацию (идентификатор потока), что делает его медленнее. Рекурсивные мьютексы уже делают это, вы должны их вместо этого.
StaceyGirl
9

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

Это совершенно не нужно. У меня был бы список проблем, которые нужно оптимизировать, список проблем, которые сейчас оптимизируются, и список проблем, которые были оптимизированы. (Не воспринимайте «список» буквально, подразумевайте «любую подходящую структуру данных»).

Операции добавления новой проблемы в список неоптимизированных проблем или перемещения проблемы из одного списка в другой будут выполняться под защитой одного «главного» мьютекса.

gnasher729
источник
1
Вы не думаете, что объект типа std::mutexподходит для такой структуры данных?
кв.
2
@ количество - нет. std::mutexопирается на реализацию мьютекса, определяемую операционной системой, которая вполне может принимать ресурсы (например, дескрипторы), которые ограничены и медленны в распределении и / или обработке Использование одного мьютекса для блокировки доступа к внутренней структуре данных, вероятно, будет гораздо более эффективным и, возможно, более масштабируемым.
Периата Breatta
1
Также рассмотрим переменные условия. Они могут сделать множество таких структур данных действительно легкими.
Cort Ammon - Восстановить Монику
2

Как уже говорили другие, не существует варианта использования is_lockedмьютекса, поэтому функция не существует.

Случай у вас возникли проблемы с невероятно распространенным, это в основном то , что рабочие потоки делают, которые являются одним из, если не в наиболее общей реализации потоков.

У вас есть полка с 10 ящиками на нем. У вас есть 4 рабочих, работающих с этими коробками. Как убедиться, что 4 рабочих работают на разных боксах? Первый рабочий снимает коробку с полки, прежде чем начать над ней работать. Второй рабочий видит 9 полок на полке.

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

Питер - Унбан Роберт Харви
источник
1

В дополнение к двум причинам, приведенным в ответе 5gon12eder выше, я хотел бы добавить, что это не является ни необходимым, ни желательным.

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

Если вам нужен доступ к общему ресурсу, защищенному мьютексом, и вы еще не держите мьютекс, вам нужно приобрести мьютекс. Другого варианта нет, иначе ваша логика программы не верна.
Вы можете найти блокировку приемлемой или неприемлемой, в любом случае, lock()или вы try_lock()получите желаемое поведение. Все, что вам нужно знать, положительно и без сомнения, это то, успешно ли вы приобрели мьютекс (возвращаемое значение try_lockговорит вам). Неважно, держит ли его кто-то другой или у вас есть ложный сбой.

Во всех остальных случаях, прямо, это не ваше дело. Вам не нужно знать, и вы не должны знать, или делать предположения (для проблем своевременности и синхронизации, упомянутых в другом вопросе).

Damon
источник
1
Что если я захочу выполнить операцию ранжирования ресурсов, которые в настоящее время доступны для блокировки?
кв.
Но реально ли это случиться? Я бы посчитал это довольно необычным. Я бы сказал, что либо у ресурсов уже есть какой-то внутренний вид ранжирования, тогда вам нужно сначала сделать (приобрести блокировку) более важный. Пример: необходимо обновить физическую симуляцию перед рендерингом. Или ранжирование является более или менее преднамеренным, тогда вы также можете try_lockиспользовать первый ресурс, а в случае неудачи попробуйте второй. Пример: три постоянных пула соединения с сервером базы данных, и вам нужно использовать одно для отправки команды.
Деймон
4
@quant - «операция ранжирования ресурсов, которые в настоящее время доступны для блокировки» - в общем, выполнение такого рода действий - это действительно простой и быстрый способ написания кода, который блокируется так, как вам трудно разобраться. Детерминированность получения и разблокировки замков - почти во всех случаях лучшая политика. Поиск блокировки на основе критерия, который может измениться, порождает проблемы.
Периата Breatta
@PeriataBreatta Моя программа намеренно не определена. Теперь я вижу, что этот атрибут не является обычным, поэтому я могу понять, что отсутствие таких функций is_locked()может способствовать такому поведению.
кв.
@ Квантовое ранжирование и блокировка - это совершенно разные проблемы. Если вы хотите каким-то образом отсортировать или переупорядочить очередь с помощью блокировки, заблокируйте ее, отсортируйте, а затем разблокируйте. Если вам нужно is_locked, то существует гораздо лучшее решение вашей проблемы, чем то, которое вы имеете в виду.
Питер - Унбан Роберт Харви
1

Возможно, вы захотите использовать atomic_flag с порядком памяти по умолчанию. Он не имеет данных гонки и никогда не генерирует исключений, как mutex делает с несколькими вызовами разблокировки (и прерывает бесконтрольно, я мог бы добавить ...). В качестве альтернативы, есть атомарное (например, атомарное [bool] или атомарное [int] (с треугольными скобками, а не [])), которое имеет приятные функции, такие как load и compare_exchange_strong.

Эндрю
источник
1

Я хочу добавить вариант использования для этого: это позволило бы внутренней функции гарантировать в качестве предварительного условия / утверждения, что вызывающая сторона действительно удерживает блокировку.

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

class SynchronizedClass
{

public:

void publicFunc()
{
  std::lock_guard<std::mutex>(_mutex);

  internalFuncA();
}

// A lot of code

void newPublicFunc()
{
  internalFuncA(); // whops, forgot to acquire the lock
}


private:

void internalFuncA()
{
  assert(_mutex.is_locked_by_this_thread());

  doStuffWithLockedResource();
}

};
B3ret
источник