std :: mutex против std :: recursive_mutex как член класса

Я видел, как некоторые люди ненавидят recursive_mutex:

http://www.zaval.org/resources/library/butenhof1.html

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

Так что для объектно-ориентированного дизайна, следует std::recursive_mutex быть по умолчанию и std::mutex Рассматривается ли оптимизация производительности в общем случае, если только она не используется только в одном месте (для защиты только одного ресурса)?

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

В начале каждого публичного метода:

{
std::scoped_lock<std::recursive_mutex> sl;

40

Решение

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

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

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

например

class X {
std::mutex m;
int data;
int const max=50;

void increment_data() {
if (data >= max)
throw std::runtime_error("too big");
++data;
}
public:
X():data(0){}
int fetch_count() {
std::lock_guard<std::mutex> guard(m);
return data;
}
void increase_count() {
std::lock_guard<std::mutex> guard(m);
increment_data();
}
int increase_count_and_return() {
std::lock_guard<std::mutex> guard(m);
increment_data();
return data;
}
};

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

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

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

Это также означает, что такие вещи, как ожидание условной переменной, будут работать: если вы передаете блокировку рекурсивного мьютекса в условную переменную, то (a) вам нужно использовать std::condition_variable_any так как std::condition_variable не будет работать, и (b) будет снят только один уровень блокировки, так что вы все еще можете удерживать блокировку и, следовательно, тупик, потому что поток, который будет запускать предикат и делать уведомление, не сможет получить блокировку.

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

63

Другие решения

должен std::recursive_mutex быть по умолчанию и std::mutex рассматривается как оптимизация производительности?

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

Есть довольно распространенная ситуация, когда у вас есть:

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

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

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

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

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

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

Если вы знаете, что вам нужно, используйте один. Если вы знаете, что он вам не нужен, то использование нерекурсивной блокировки — это не просто оптимизация, это помогает обеспечить ограничение дизайна. Лучше, если вторая блокировка выйдет из строя, чем для того, чтобы она преуспела и скрыла тот факт, что вы случайно сделали что-то, что, как говорит ваш дизайн, никогда не произойдет. Но если вы будете следовать своему замыслу и никогда не блокируете мьютекс дважды, то вы никогда не узнаете, рекурсивен он или нет, и рекурсивный мьютекс не непосредственно вредны.

Эта аналогия может потерпеть неудачу, но вот другой способ взглянуть на это. Представьте, что у вас есть выбор между двумя типами указателей: один, который прерывает программу с помощью трассировки стека, когда вы разыменовываете нулевой указатель, и другой, который возвращает 0 (или расширить его на несколько типов: ведет себя так, как будто указатель ссылается на объект, инициализированный значением). Нерекурсивный мьютекс немного похож на тот, который прерывается, а рекурсивный мьютекс немного похож на тот, который возвращает 0. Они оба потенциально имеют свое применение — люди иногда идут на все, чтобы реализовать «тихий не-а». -значение «значение. Но в случае, когда ваш код разработан так, чтобы никогда не разыменовывать нулевой указатель, вы не хотите использовать по умолчанию версия, которая молча позволяет этому случиться.

23

Я не собираюсь прямо влиять на дебаты о взаимных мьютексах и recursive_mutex, но я подумал, что было бы хорошо поделиться сценарием, в котором recursive_mutex’ы абсолютно критичны для дизайна.

При работе с Boost :: asio, Boost :: coroutine (и, возможно, такими вещами, как NT Fibers, хотя я с ними менее знаком), абсолютно необходимо, чтобы ваши мьютексы были рекурсивными даже без проблемы повторного входа.

Причина в том, что основанный на сопрограмме подход по самой своей структуре приостановит выполнение внутри рутина, а затем впоследствии возобновить его. Это означает, что два метода высшего уровня класса могут «вызываться одновременно в одном потоке» без каких-либо дополнительных вызовов.

6
По вопросам рекламы [email protected]