После паники потока, удерживающего std::sync::Mutex, что произойдёт при следующей попытке захватить этот mu...

После паники потока, удерживающего std::sync::Mutex, что произойдёт при следующей попытке захватить этот mutex?

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

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

При панике потока, если он владел MutexGuard, mutex обычно помечается как poisoned. Следующий вызов lock не возвращает обычный успешный результат: он выдаёт Err(PoisonError), хотя защищённый mutex уже может быть разблокирован.

Это не означает повреждение памяти или автоматическое завершение программы. Отказ можно обработать, извлечь guard из PoisonError и самостоятельно решить, допустимо ли продолжать работу с состоянием.

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

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

Подход отделяет две задачи: mutex предотвращает одновременный доступ, а poisoning сообщает о возможной логической неконсистентности состояния. Это компромисс между автоматическим продолжением работы и принудительным завершением процесса.

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

Предположим, критическая секция обновляет несколько полей структуры, которые должны изменяться согласованно. Если внутри этой секции возникает паника, guard освобождается во время раскрутки стека, но обновление может остаться незавершённым.

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

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

Когда guard уничтожается во время раскрутки стека после паники, std::sync::Mutex обычно устанавливает флаг poisoning. При следующем lock результат имеет тип Result<MutexGuard<T>, PoisonError<MutexGuard<T>>>.

Из PoisonError можно получить guard через into_inner. Это одновременно захватывает mutex и предоставляет доступ к данным, несмотря на poisoning. После проверки или исправления инвариантов mutex можно очистить методом clear_poison.

use std::sync::{Arc, Mutex}; use std::thread; fn main() { let value = Arc::new(Mutex::new(0)); let worker_value = Arc::clone(&value); let _ = thread::spawn(move || { let mut guard = worker_value.lock().unwrap(); *guard = 42; panic!("сбой"); }).join(); match value.lock() { Ok(guard) => println!("{guard}"), Err(poisoned) => { let guard = poisoned.into_inner(); println!("восстановление: {guard}"); } } }

Важное ограничение: poisoning не является механизмом защиты от неопределённого поведения и не гарантирует, что состояние действительно некорректно. Это диагностический и координационный сигнал; код восстановления должен понимать инварианты конкретного типа.

Если приложение использует стратегию panic = abort, раскрутки стека не происходит, поэтому обработать последствия паники внутри процесса нельзя. Для try_lock аналогичная ситуация представляется вариантом TryLockError::Poisoned, если mutex не занят другим владельцем.

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

В сервисе поток обновляет баланс и журнал операции под одним mutex. При ошибке сериализации внутри критической секции поток паникует, а следующий запрос получает PoisonError.

Вариант с безусловным unwrap прост, но приводит к панике и цепной остановке рабочих потоков. Безусловное использование into_inner сохраняет доступность сервиса, однако может распространить частично обновлённый баланс.

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

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

  1. Означает ли poisoning, что mutex остаётся заблокированным?

Нет. Во время паники guard освобождается, поэтому блокировка обычно становится доступной другим потокам. Poisoning — это отдельный флаг состояния, из-за которого последующий lock возвращает ошибку вместо обычного Ok.

  1. Можно ли безопасно игнорировать PoisonError?

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

  1. Снимается ли poisoning автоматически после успешного восстановления?

Нет. Получение guard через into_inner не очищает флаг автоматически. Если состояние проверено и исправлено, код может явно вызвать clear_poison; иначе последующие попытки захвата продолжат получать признак poisoning.