Программирование C++МногопоточностьC++ разработчик многопоточных систем

Ручной захват мьютекса прерван исключением до вызова unlock — как это скажется на следующем захвате?

Ручной захват мьютекса прерван исключением до вызова unlock — как это скажется на следующем захвате?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Если исключение прервало выполнение между захватом мьютекса и ручным вызовом unlock, мьютекс останется захваченным. Следующий поток, пытающийся выполнить обычный lock, может заблокироваться навсегда. Для гарантированного освобождения используют RAII, например std::lock_guard или std::unique_lock.

Исторический контекст

Ручное управление ресурсами плохо сочетается с исключениями: управление может покинуть функцию по нескольким путям, включая return и аварийное завершение через исключение. Идиома RAII связывает время жизни ресурса с объектом, чтобы его освобождение выполнялось автоматически при выходе из области видимости.

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

Постановка проблемы

После успешного захвата std::mutex поток становится его владельцем. Пока не выполнен соответствующий unlock, другие потоки не смогут захватить этот мьютекс.

Если между lock и unlock возникает исключение, обычный последовательный код до unlock не дойдёт. В результате появляется утечка блокировки: другие потоки могут ждать бесконечно, а защищаемая операция или вся программа — зависнуть.

Подробное решение

Объект std::lock_guard захватывает мьютекс при конструировании и освобождает его в деструкторе. При раскрутке стека из-за исключения деструктор вызывается автоматически, поэтому блокировка снимается даже при досрочном выходе.

#include <mutex> #include <stdexcept> void update(std::mutex& m, int& value) { std::lock_guard<std::mutex> lock(m); ++value; throw std::runtime_error{}; }

После уничтожения lock другой поток сможет захватить m. Кроме освобождения мьютекса, успешная разблокировка и последующий успешный захват того же мьютекса обеспечивают отношение synchronizes-with, поэтому изменения обычных данных, выполненные до unlock, становятся видимыми потоку после его lock.

std::unique_lock выбирают, когда нужны дополнительные операции: отложенный захват, явный временный unlock, повторный захват или передача владения. Однако эти возможности увеличивают число состояний, которые нужно правильно контролировать; для простой критической секции предпочтительнее std::lock_guard.

Нельзя полагаться на ручной unlock в обработчике исключения как на универсальное решение: обработчик может быть пропущен, вложенные вызовы могут выбросить другие исключения, а поддержание симметрии lock и unlock становится хрупким. Также нельзя копировать lock_guard или освобождать мьютекс из потока, который не является его владельцем.

Ситуация из практики

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

Рассматривались два варианта. Добавление ручного unlock в нескольких блоках catch устраняло отдельные пути ошибки, но оставляло риск пропустить новый путь при дальнейшем изменении функции. Замена мьютекса на рекурсивный также не решала проблему: рекурсивность разрешает повторный захват владельцем, но не освобождает мьютекс после исключения.

Был выбран std::lock_guard на всю критическую секцию, а потенциально исключающую операцию журналирования вынесли за пределы блокировки. В результате мьютекс освобождался при любом выходе, а длительность критической секции дополнительно сократилась.

Что кандидаты часто упускают

  1. Освободится ли мьютекс автоматически, если исключение перехватывается выше по стеку?

Да, если мьютекс принадлежит объекту RAII, например std::lock_guard. При раскрутке стека деструктор локального объекта вызывается до перехода в обработчик исключения, находящийся выше. Если же мьютекс был захвачен вручную, автоматического освобождения не будет.

  1. Решает ли проблему использование std::recursive_mutex?

Нет. std::recursive_mutex разрешает одному потоку захватывать один мьютекс несколько раз, обычно с соответствующим числом освобождений. Он не освобождает мьютекс при исключении и не устраняет риск взаимной блокировки или ошибочной структуры критической секции. Для управления временем владения по-прежнему нужен RAII.

  1. Когда вместо std::lock_guard оправдан std::unique_lock?

std::unique_lock нужен, если блокировку требуется захватить не сразу, временно отпустить, снова захватить или передать объекту, например условной переменной. Он отслеживает, владеет ли мьютексом, и освобождает его в деструкторе только при наличии владения. Если таких возможностей не требуется, lock_guard проще и лучше выражает намерение.