Что произойдёт, если повторно заблокировать (lock) std::mutex в том же потоке?

Ответ

Повторная попытка захватить нерекурсивный мьютекс (std::mutex) в одном и том же потоке приводит к неопределённому поведению (Undefined Behavior, UB). На практике это чаще всего вызывает взаимную блокировку (deadlock), так как поток бесконечно ждёт сам себя.

#include <mutex>

std::mutex mtx;

void risky_function() {
    mtx.lock();   // Первый захват – OK.
    // ... какая-то работа ...
    mtx.lock();   // ВТОРОЙ захват в том же потоке – НЕОПРЕДЕЛЁННОЕ ПОВЕДЕНИЕ (deadlock!).
    mtx.unlock();
    mtx.unlock();
}

Решение: Для сценариев, где один поток может нуждаться в многократном захвате одного и того же мьютекса, используйте рекурсивный мьютекс std::recursive_mutex.

#include <mutex>

std::recursive_mutex rec_mtx;

void safe_recursive_function(int depth) {
    rec_mtx.lock(); // Захват 1 (или N) – OK.
    // ... работа ...
    if (depth > 0) {
        safe_recursive_function(depth - 1); // Рекурсивный вызов снова захватит мьютекс.
    }
    rec_mtx.unlock(); // Каждому lock() должен соответствовать unlock().
}

Важно: Использование std::recursive_mutex часто указывает на проблему с дизайном кода. По возможности следует пересматривать структуру функций и область блокировки, чтобы избежать необходимости в рекурсивных блокировках.

Ответ 18+ 🔞

Э, слушай, вот тебе история про мьютексы, которая вгоняет в ступень даже бывалых. Представь, что у тебя есть дверь в туалет, и ты её изнутри закрыл на засов. А потом, сидя на унитазе, пытаешься снова её закрыть. Ну и как, ёпта? Ты же уже внутри! Ты будешь вечно ждать, пока кто-то изнутри откроет, но это же ты сам и есть! Вот это и есть классический deadlock на ровном месте.

Вот смотри, обычный std::mutex — он как раз такой засов. Один поток его захватил — всё, второй раз в том же потоке его не трогай, а то будет неопределённое поведение (UB), а на практике — просто зависнешь нахуй.

#include <mutex>

std::mutex mtx;

void risky_function() {
    mtx.lock();   // Захватил, молодца. Сиди, работай.
    // ... какая-то работа ...
    mtx.lock();   // А вот это, блядь, пиздец. Ты чё, охренел? Ты же уже его держишь! Теперь ты ждёшь сам себя до скончания веков. Программа встала колом.
    mtx.unlock();
    mtx.unlock();
}

Удивление пиздец, правда? Казалось бы, логично, но нет. Компилятор тебе не мама, он не скажет "чувак, опомнись". Просто накроется медным тазом всё.

Решение? Да есть же, блядь, специальная штука — рекурсивный мьютекс (std::recursive_mutex). Это как засов с внутренней памятью. Он помнит, какой поток его взял и сколько раз. Захотел войти в ту же дверь повторно — пожалуйста, проходи, только потом столько же раз и выйди.

#include <mutex>

std::recursive_mutex rec_mtx;

void safe_recursive_function(int depth) {
    rec_mtx.lock(); // Взял раз. Ок.
    // ... работа ...
    if (depth > 0) {
        safe_recursive_function(depth - 1); // Вошёл в себя рекурсивно — мьютекс взялся ВТОРОЙ раз в том же потоке. И НИЧЕГО! Не deadlock!
    }
    rec_mtx.unlock(); // И отпускать надо ровно столько же раз, сколько взял. А то сосед не попадёт.
}

Но вот что важно, чувак: если ты начал юзать recursive_mutex, это часто хитрая жопа, которая кричит, что с дизайном твоего кода что-то не так. Это как костыль. В идеале нужно перелопатить логику, чтобы не было нужды хватать один и тот же мьютекс из одного потока много раз. А то получится манда с ушами — вроде работает, но доверия ебать ноль и потом разбираться, кто что и когда отпускает, — это тот ещё пиздец.