← Назад к вопросам

Что случится, если повторно заблокировать std::mutex в том же потоке?

2.0 Middle🔥 101 комментариев
#Многопоточность и синхронизация#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что случится при повторной блокировке std::mutex в том же потоке

Ответ: DEADLOCK

std::mutex не является recursive (переусловный). Если один поток попытается заблокировать мьютекс, который он уже держит, произойдет deadlock — поток заблокируется самого себя и будет ждать неопределённо.

Пример проблемы

#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx;

void problematic_function() {
    mtx.lock();  // Первая блокировка — успешна
    std::cout << "First lock acquired" << std::endl;
    
    mtx.lock();  // Вторая блокировка — DEADLOCK!
    std::cout << "This will never print" << std::endl;
    
    mtx.unlock();
    mtx.unlock();
}

int main() {
    problematic_function();
    return 0;
}

Результат: программа зависнет. Второй lock() никогда не вернёт управление.

Почему это происходит

Мьютекс — это примитив синхронизации с состояниями:

  • Unlocked — свободен
  • Locked — заблокирован потоком A

Когда поток A держит мьютекс:

  1. Попытка другого потока заблокировать → ждёт (correct)
  2. Попытка того же потока A заблокировать → ждёт того, что уже имеет (deadlock)

Решение 1: std::recursive_mutex

#include <mutex>

std::recursive_mutex mtx;  // Может быть заблокирован много раз

void safe_recursive_function(int depth) {
    mtx.lock();
    std::cout << "Depth: " << depth << std::endl;
    
    if (depth > 0) {
        safe_recursive_function(depth - 1);  // Рекурсивный вызов
    }
    
    mtx.unlock();
}

int main() {
    safe_recursive_function(3);
    return 0;
}

Как работает:

  • recursive_mutex ведёт счётчик блокировок
  • Один поток может заблокировать его N раз
  • Требует N unlock() для полного освобождения
mtx.lock();    // counter = 1
mtx.lock();    // counter = 2
mtx.lock();    // counter = 3
mtx.unlock();  // counter = 2
mtx.unlock();  // counter = 1
mtx.unlock();  // counter = 0 (освобожден)

Решение 2: std::unique_lock (рекомендуется)

#include <mutex>

std::mutex mtx;

void safe_function() {
    std::unique_lock<std::mutex> lock(mtx);
    std::cout << "Lock acquired" << std::endl;
    
    // lock будет автоматически освобожден при выходе из scope
    // Даже если выбросится исключение
}

Преимущества:

  • RAII — автоматическое освобождение
  • Исключения не вызывают утечку блокировки
  • Более современный подход

Решение 3: std::lock_guard (самое простое)

#include <mutex>

std::mutex mtx;

void safe_function() {
    std::lock_guard<std::mutex> guard(mtx);
    // guard автоматически разблокирует мьютекс при выходе из scope
}

Сравнение решений

ПодходРекурсияПроизводительностьРекомендуется
std::mutexНет (deadlock)Очень быстроДля non-recursive кода
std::recursive_mutexДаМедленнееРедко, обычно признак bad design
std::unique_lockЗависитНормальноОбычно лучший выбор
std::lock_guardЗависитОчень быстроЕсли не нужна гибкость

Чекер на практике

#include <mutex>
#include <memory>
#include <iostream>

class DataContainer {
private:
    std::mutex mtx;
    int data = 0;

public:
    void increment() {
        std::lock_guard<std::mutex> guard(mtx);
        data++;
    }
    
    void print_and_increment() {
        std::lock_guard<std::mutex> guard(mtx);
        std::cout << "Data: " << data << std::endl;
        // Обычно НЕ вызываем increment() внутри,
        // так как это вызовет deadlock с std::mutex
        // Лучше вынести логику в отдельную функцию
        data++;  // Вместо этого делаем прямую операцию
    }
};

Почему std::recursive_mutex редко используется

  1. Признак плохого дизайна — если функция часто нужна рекурсивно, переработайте архитектуру
  2. Медленнее — требует overhead на ведение счётчика
  3. Легче ошибиться — можно забыть про счётчик
  4. Усложняет отладку — deadlock вероятнее

Правило золотой середины

// ❌ Плохо: используешь recursive_mutex везде
std::recursive_mutex mtx;  // Антипаттерн

// ✅ Хорошо: переструктурируешь код
std::mutex mtx;  // Обычный мьютекс

void internal_impl_locked() {
    // Внутренняя реализация, предполагает что мьютекс уже заблокирован
}

void public_function() {
    std::lock_guard<std::mutex> guard(mtx);
    internal_impl_locked();
}

Итоговый ответ

Если повторно заблокировать std::mutex в том же потоке:

  • Произойдет deadlock
  • Поток зависнет на неопределённое время
  • Программа может зависнуть полностью

Решение:

  1. Переструктурировать код — refactor чтобы не было рекурсии
  2. Использовать std::recursive_mutex — если действительно необходимо
  3. Использовать RAII (std::lock_guard, std::unique_lock) — для безопасности

Правило: избегай рекурсивного захвата мьютекса. Если система требует этого — переработай архитектуру.

Что случится, если повторно заблокировать std::mutex в том же потоке? | PrepBro