Какой тип мьютекса можно повторно блокировать в том же потоке?

Ответ

В C++ для этого используется рекурсивный мьютекс (std::recursive_mutex). В отличие от обычного std::mutex, который при повторной попытке захвата тем же потоком приводит к неопределённому поведению или deadlock, рекурсивный мьютекс позволяет потоку многократно захватывать одну и ту же блокировку.

Пример использования:

#include <iostream>
#include <mutex>

std::recursive_mutex rec_mutex;
int shared_data = 0;

void recursive_function(int depth) {
    std::lock_guard<std::recursive_mutex> lock(rec_mutex); // Захват #1
    if (depth > 0) {
        // Внутри критической секции снова нужен доступ к защищённым данным
        std::lock_guard<std::recursive_mutex> lock_again(rec_mutex); // Захват #2, разрешён
        shared_data += depth;
        recursive_function(depth - 1); // Рекурсивный вызов также захватит мьютекс
    }
}

int main() {
    recursive_function(3);
    std::cout << "Shared data: " << shared_data << std::endl; // Вывод: Shared data: 6
    return 0;
}

Важные детали:

  • Каждому успешному вызову lock() (явному или через std::lock_guard/std::unique_lock) должен соответствовать вызов unlock(). Рекурсивный мьютекс ведёт внутренний счётчик.
  • Использовать std::recursive_mutex стоит только когда это действительно необходимо (например, при вызове общедоступных методов класса, которые сами блокируются, из другого метода этого же класса). Часто архитектуру кода можно пересмотреть, чтобы избежать рекурсивных блокировок.
  • Для автоматического управления временем жизни блокировки всегда предпочитайте RAII-обёртки (std::lock_guard, std::unique_lock).

Ответ 18+ 🔞

Смотри, бывает такая ситуация — пишешь ты код, всё логично, а потом бац — и сам себя заблокировал. Ну, как будто в туалет зашёл, дверь на ключ закрыл, а ключ-то внутри остался. И стоишь такой, охуевший, в полном недоумении. Так вот, чтобы с тобой такого не случилось, в C++ придумали рекурсивный мьютекс (std::recursive_mutex).

Обычный std::mutex — он строгий, блядь. Захватил его один раз в потоке и всё, второй раз уже не даст. Попробуешь — получишь deadlock, undefined behavior, и вообще, ёперный театр. А рекурсивный — он поумнее, он разрешает одному и тому же потоку захватывать себя несколько раз. Как будто у тебя ключ-дубликат в кармане.

Смотри, как это выглядит на практике:

#include <iostream>
#include <mutex>

std::recursive_mutex rec_mutex;
int shared_data = 0;

void recursive_function(int depth) {
    std::lock_guard<std::recursive_mutex> lock(rec_mutex); // Захват #1
    if (depth > 0) {
        // Внутри критической секции снова нужен доступ к защищённым данным
        std::lock_guard<std::recursive_mutex> lock_again(rec_mutex); // Захват #2, разрешён
        shared_data += depth;
        recursive_function(depth - 1); // Рекурсивный вызов также захватит мьютекс
    }
}

int main() {
    recursive_function(3);
    std::cout << "Shared data: " << shared_data << std::endl; // Вывод: Shared data: 6
    return 0;
}

Теперь про важные детали, а то накосячишь:

  • Запомни, как отче наш: сколько раз вызвал lock() (сам или через std::lock_guard), столько же раз должен быть и unlock(). Этот мьютекс внутри считает, как сумасшедший. Не сбалансируешь — пиши пропало.
  • Использовать эту штуку нужно с умом, а не потому что лень архитектуру продумать. Часто, если тебе реально нужен рекурсивный мьютекс, это знак, что код пора пересмотреть. Это как костыль — помогает идти, но хромать от этого не перестаёшь.
  • И ради всего святого, не выёбывайся — всегда используй RAII-обёртки (std::lock_guard, std::unique_lock). Они сами всё закроют, даже если твоя функция внезапно кинет исключение. А иначе будешь потом искать, где же этот чёртов unlock не вызвался, и волосы на жопе выдирать.