Ручной захват мьютекса прерван исключением до вызова unlock — как это скажется на следующем захвате?
Если исключение прервало выполнение между захватом мьютекса и ручным вызовом unlock, мьютекс останется захваченным. Следующий поток, пытающийся выполнить обычный lock, может заблокироваться навсегда. Для гарантированного освобождения используют RAII, например std::lock_guard или std::unique_lock.
Ручное управление ресурсами плохо сочетается с исключениями: управление может покинуть функцию по нескольким путям, включая return и аварийное завершение через исключение. Идиома RAII связывает время жизни ресурса с объектом, чтобы его освобождение выполнялось автоматически при выходе из области видимости.
В многопоточности этот подход особенно важен для мьютексов: ошибка освобождения блокировки обычно проявляется не сразу, а как зависание другого потока.
После успешного захвата std::mutex поток становится его владельцем. Пока не выполнен соответствующий unlock, другие потоки не смогут захватить этот мьютекс.
Если между lock и unlock возникает исключение, обычный последовательный код до unlock не дойдёт. В результате появляется утечка блокировки: другие потоки могут ждать бесконечно, а защищаемая операция или вся программа — зависнуть.
Объект std::lock_guard захватывает мьютекс при конструировании и освобождает его в деструкторе. При раскрутке стека из-за исключения деструктор вызывается автоматически, поэтому блокировка снимается даже при досрочном выходе.
После уничтожения lock другой поток сможет захватить m. Кроме освобождения мьютекса, успешная разблокировка и последующий успешный захват того же мьютекса обеспечивают отношение synchronizes-with, поэтому изменения обычных данных, выполненные до unlock, становятся видимыми потоку после его lock.
std::unique_lock выбирают, когда нужны дополнительные операции: отложенный захват, явный временный unlock, повторный захват или передача владения. Однако эти возможности увеличивают число состояний, которые нужно правильно контролировать; для простой критической секции предпочтительнее std::lock_guard.
Нельзя полагаться на ручной unlock в обработчике исключения как на универсальное решение: обработчик может быть пропущен, вложенные вызовы могут выбросить другие исключения, а поддержание симметрии lock и unlock становится хрупким. Также нельзя копировать lock_guard или освобождать мьютекс из потока, который не является его владельцем.
В обработчике запроса поток захватывал общий мьютекс, обновлял состояние и вызывал функцию журналирования. Иногда журналирование выбрасывало исключение, после чего следующий запрос зависал на захвате того же мьютекса.
Рассматривались два варианта. Добавление ручного unlock в нескольких блоках catch устраняло отдельные пути ошибки, но оставляло риск пропустить новый путь при дальнейшем изменении функции. Замена мьютекса на рекурсивный также не решала проблему: рекурсивность разрешает повторный захват владельцем, но не освобождает мьютекс после исключения.
Был выбран std::lock_guard на всю критическую секцию, а потенциально исключающую операцию журналирования вынесли за пределы блокировки. В результате мьютекс освобождался при любом выходе, а длительность критической секции дополнительно сократилась.
Да, если мьютекс принадлежит объекту RAII, например std::lock_guard. При раскрутке стека деструктор локального объекта вызывается до перехода в обработчик исключения, находящийся выше. Если же мьютекс был захвачен вручную, автоматического освобождения не будет.
std::recursive_mutex?Нет. std::recursive_mutex разрешает одному потоку захватывать один мьютекс несколько раз, обычно с соответствующим числом освобождений. Он не освобождает мьютекс при исключении и не устраняет риск взаимной блокировки или ошибочной структуры критической секции. Для управления временем владения по-прежнему нужен RAII.
std::lock_guard оправдан std::unique_lock?std::unique_lock нужен, если блокировку требуется захватить не сразу, временно отпустить, снова захватить или передать объекту, например условной переменной. Он отслеживает, владеет ли мьютексом, и освобождает его в деструкторе только при наличии владения. Если таких возможностей не требуется, lock_guard проще и лучше выражает намерение.