std:: mutex vs std:: рекурсивный мьютекс как член класса
Я видел, как некоторые люди ненавидят на recursive_mutex
:
http://www.zaval.org/resources/library/butenhof1.html
но когда вы думаете о том, как реализовать класс, который является потокобезопасным (защищенным мьютексом), мне кажется мучительно трудно доказать, что каждый метод, который должен быть защищен мьютексом, защищен мьютексом и что мьютекс заблокирован не более одного раза.
поэтому для объектно-ориентированного дизайна следует std::recursive_mutex
быть по умолчанию и std::mutex
рассмотрел как оптимизация производительности в общем случае, если она не используется только в одном месте (для защиты только одного ресурса)?
чтобы прояснить ситуацию, я говорю об одном частном нестатическом мьютексе. Таким образом, каждый экземпляр класса имеет только один мьютекс.
в начале каждого публичного метода:
{
std::scoped_lock<std::recursive_mutex> sl;
3 ответов
большую часть времени, если вы думаете, что вам нужен рекурсивный мьютекс, то ваш дизайн неправильный, поэтому он определенно не должен быть по умолчанию.
для класса с одним мьютексом, защищающим члены данных, то мьютекс должен быть заблокирован во всех 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) только один уровень замка выпущен, поэтому вы может по-прежнему удерживать блокировку и, следовательно, взаимоблокировку, поскольку поток, который запускает предикат и делает уведомление, не может получить блокировку.
я пытаюсь придумать сценарий, в котором требуется рекурсивный мьютекс.
должны
std::recursive_mutex
быть по умолчанию иstd::mutex
рассматривается как оптимизация производительности?
на самом деле нет. Преимущество использования нерекурсивных блокировок -не просто оптимизация производительности, это означает, что ваш код самостоятельно проверяет, что атомарные операции на уровне листа действительно являются на уровне листа, они не вызывают что-то еще, что использует блокировку.
есть достаточно распространенная ситуация, когда вы есть:
- функция, которая реализует некоторую операцию, которая должна быть сериализована, поэтому она берет мьютекс и делает это.
- другая функция, которая реализует большую сериализованную операцию, и хочет вызвать первую функцию, чтобы сделать один шаг, в то время как она удерживает блокировку для большей операции.
для конкретного примера, возможно, первая функция атомарно удаляет узел из списка, в то время как вторая функция атомарно удаляет два узлы из списка (и вы никогда не хотите, чтобы другой поток видел список только с одним из двух узлов).
не нужно рекурсивные мьютексы для этого. Например, вы можете рефакторировать первую функцию как общедоступную функцию, которая принимает блокировку и вызывает частную функцию, которая выполняет операцию "небезопасно". Затем вторая функция может вызвать ту же функцию private.
однако иногда это удобно использовать рекурсивный мьютекс. Все еще есть проблема с этим дизайном:remove_two_nodes
звонки remove_one_node
в точке, где инвариант класса не выполняется (во второй раз он вызывает его, список находится в точно таком состоянии, которое мы не хотим выставлять). Но если предположить, что мы это знаем ... --3--> не полагается на этот инвариант, это не ошибка убийцы в дизайне, это просто то, что мы сделали наши правила немного сложнее, чем идеал " все инварианты класса всегда держатся, когда любая публичная функция внесенный."
Итак, трюк иногда полезен, и я не ненавижу рекурсивные мьютексы в той степени, в какой это делает статья. У меня нет исторических знаний, чтобы утверждать, что причина их включения в Posix отличается от того, что говорится в статье, "чтобы продемонстрировать атрибуты мьютекса и расширения потоков". Я, конечно, не считаю их дефолтом.
Я думаю, можно с уверенностью сказать, что если в вашем дизайне вы не уверены, нужна ли вам рекурсивная блокировка или нет, тогда ваш проект неполон. Позже вы пожалеете о том, что пишете код и вы не знаю что-то настолько фундаментально важное, как то, разрешено ли уже удерживать замок или нет. Поэтому не ставьте рекурсивный замок "на всякий случай".
если вы знаете, что вам нужно использовать один. Если вы знаете, что он вам не нужен, то использование нерекурсивной блокировки-это не просто оптимизация, это помогает обеспечить ограничение дизайна. Это более полезно для второго замка, чтобы потерпеть неудачу, чем для его успеха и скрыть тот факт, что вы случайно сделали то, что ваш дизайн говорит никогда не должно произойти. Но если вы будете следовать своему дизайну и никогда не будете дважды блокировать мьютекс, вы никогда не узнаете, рекурсивен он или нет, и поэтому рекурсивный мьютекс не напрямую вредно.
эта аналогия может потерпеть неудачу, но вот другой способ взглянуть на нее. Представьте, что у вас есть выбор между двумя типами указателей: тот, который прерывает программа с stacktrace при разыменовании нулевого указателя и другого, который возвращает 0
(или расширить его на другие типы: ведет себя так, как будто указатель ссылается на инициализированный значением объект). Нерекурсивный мьютекс немного похож на тот, который прерывает, а рекурсивный мьютекс немного похож на тот, который возвращает 0. Они оба потенциально имеют свое применение - люди иногда идут на некоторые длины, чтобы реализовать значение" тихий не-значение". Но в случае, когда ваш код предназначен для never разыменование нулевого указателя, вы не хотите использовать по умолчанию версия, которая молча позволяет этому произойти.
Я не собираюсь напрямую взвешивать мьютекс против recursive_mutex, но я подумал, что было бы неплохо поделиться сценарием, в котором recursive_mutex абсолютно критичны для дизайна.
при работе с Boost::asio, Boost:: coroutine (и, вероятно, такие вещи, как NT-волокна, хотя я менее знаком с ними), абсолютно важно, чтобы ваши мьютексы были рекурсивными даже без проблемы дизайна повторного входа.
причина в том, что подход на основе coroutine по самой своей конструкции приостановит выполнение внутри обычной, а затем возобновить его. Это означает, что два метода верхнего уровня класса могут "вызываться одновременно в одном потоке" без каких-либо дополнительных вызовов.