Что произойдёт при выполнении функции одним потоком, если обычный мьютекс повторно захватывается тем же владельцем?
#include <mutex>
void update(std::mutex& m, int& value) {
std::lock_guard<std::mutex> first(m);
++value;
if (value == 1) {
std::lock_guard<std::mutex> second(m);
value = 2;
}
}
Повторный захват std::mutex тем же потоком недопустим: стандарт не гарантирует корректного поведения, а на практике поток обычно блокируется сам на себе. У обычного мьютекса нет рекурсивной семантики, поэтому до присваивания value = 2 выполнение, как правило, не дойдёт.
Мьютекс предназначен для защиты критической секции, в которой одновременно работает только один поток. Разделение обычного и рекурсивного мьютекса позволяет явно выбирать модель владения: обычный мьютекс выявляет повторный вход, а std::recursive_mutex допускает несколько захватов одним владельцем.
Обычный мьютекс является предпочтительным базовым примитивом: он не скрывает структуру блокировок и помогает обнаруживать ошибочную реентерабельность. Рекурсивность добавляет возможность повторного входа, но не делает автоматически безопасными защищаемые инварианты.
В примере поток уже владеет m, когда создаётся объект second. Второй lock_guard вызывает m.lock(), но мьютекс ожидает освобождения, которое может выполнить только текущий поток после выхода из внешнего lock_guard.
На практике это обычно выглядит как взаимная блокировка одного потока с самим собой. Однако полагаться именно на такое наблюдаемое поведение нельзя: для повторного захвата std::mutex уже принадлежащего текущему потоку стандарт не обещает нормальный результат.
Нужно устранить повторный захват, а не пытаться «дождаться» разблокировки. Обычно внутреннюю часть функции выносят в helper, который предполагает, что мьютекс уже захвачен, и вызывают его только из внешней критической секции.
Если повторный вход действительно является частью протокола объекта, можно использовать std::recursive_mutex. Он ведёт счёт захватов текущего владельца, и мьютекс освобождается только после соответствующего числа unlock, но это решение следует применять осознанно: оно может скрыть ошибочную архитектуру и усложнить проверку инвариантов.
Для публичных методов полезно различать методы, которые захватывают мьютекс, и внутренние методы с суффиксом вроде _locked, не захватывающие его повторно. Вызов внешнего пользовательского callback под удерживаемым мьютексом также опасен: callback может реентерабельно вызвать тот же объект или создать цикл блокировок.
Класс хранит состояние под мьютексом и во время изменения вызывает callback наблюдателя. Наблюдатель обращается к тому же публичному методу, который снова пытается захватить мьютекс. Замена std::mutex на std::recursive_mutex устраняет самоблокировку, но оставляет объект в промежуточном состоянии и позволяет callback увидеть неконсистентные данные.
Вариант с рекурсивным мьютексом прост, но скрывает реентерабельность и обычно имеет менее прозрачную модель владения. Вариант с ручным unlock перед callback уменьшает риск блокировки, однако требует сначала скопировать данные для callback и тщательно продумать, что изменится между разблокировкой и уведомлением.
Предпочтительное решение — изменить состояние под обычным мьютексом, скопировать необходимые данные, освободить мьютекс и только затем вызвать callback. Такой подход сохраняет инвариант во время публикации, не удерживает блокировку во внешнем коде и предотвращает повторный захват того же мьютекса.
std::mutex на std::recursive_mutex без других изменений?Не всегда. Рекурсивный мьютекс устраняет самоблокировку только на уровне захватов, но не исправляет реентерабельность. Повторный вызов может наблюдать частично обновлённое состояние, вызвать callback в неподходящий момент или нарушить логический инвариант. Поэтому такая замена допустима лишь при явно спроектированной рекурсивной модели.
lock использовать try_lock?Нельзя считать это универсальным способом обнаружить самоблокировку. Для уже принадлежащего текущему потоку std::mutex стандарт не гарантирует корректное поведение повторной попытки захвата; рассчитывать на обязательный результат false нельзя. Кроме того, проверка через try_lock не устраняет логическую проблему повторного входа и может породить ветку с частично выполненной операцией.
std::mutex не копируется и не перемещается, поскольку копирование владения разрушило бы смысл примитива синхронизации. Вспомогательной функции передают ссылку или указатель на тот же мьютекс, но это не должно приводить к повторному захвату. Часто безопаснее передавать в закрытый helper только защищаемые данные и явно документировать, что helper вызывается исключительно при уже удерживаемой блокировке.