Какие виды мьютексов знаешь в C++?

Ответ

Стандартная библиотека C++ предоставляет несколько типов мьютексов, каждый для конкретных сценариев синхронизации:

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

    std::mutex mtx;
    std::vector<int> shared_vec;
    
    void safe_push(int val) {
        std::lock_guard<std::mutex> lock(mtx); // RAII-захват
        shared_vec.push_back(val);
    } // Автоматическая разблокировка при выходе из области видимости
  2. std::recursive_mutex — рекурсивный мьютекс. Позволяет одному и тому же потоку захватывать его несколько раз. Необходимо вызвать unlock() столько же раз, сколько был вызван lock().

    std::recursive_mutex rec_mtx;
    void recursive_func(int x) {
        std::lock_guard<std::recursive_mutex> lock(rec_mtx);
        if (x > 0) {
            recursive_func(x - 1); // Безопасный рекурсивный вызов
        }
    }
  3. std::timed_mutex — расширяет std::mutex, добавляя методы try_lock_for() и try_lock_until(), которые позволяют попытаться захватить мьютекс с таймаутом.

    std::timed_mutex tmtx;
    if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {
        // Захватили за 100 мс
        tmtx.unlock();
    } else {
        // Не удалось захватить, выполняем альтернативный код
    }
  4. std::recursive_timed_mutex — комбинация возможностей рекурсивного и timed мьютекса.

  5. std::shared_mutex (C++17) — мьютекс для реализации паттерна "множество читателей, один писатель". Позволяет нескольким потокам одновременно захватывать его в режиме разделяемой блокировки (lock_shared()) для чтения, но для записи требуется эксклюзивная блокировка (lock()).

    std::shared_mutex sh_mtx;
    std::string shared_data;
    
    // Поток-читатель
    {
        std::shared_lock lock(sh_mtx); // Разделяемая блокировка
        std::cout << shared_data;
    }
    
    // Поток-писатель
    {
        std::unique_lock lock(sh_mtx); // Эксклюзивная блокировка
        shared_data = "new value";
    }

Рекомендация: Всегда предпочитайте RAII-обёртки (std::lock_guard, std::unique_lock, std::shared_lock) для управления блокировками, чтобы избежать утечки блокировок при исключениях.

Ответ 18+ 🔞

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

Вот первый, std::mutex, — это как базовый солдат, тупой и прямолинейный. Захватил — твой, пока не отпустишь. Но ёпта, главное правило: если поток, который его уже держит, вдруг опять попробует захватить, будет тебе пиздец, обычно deadlock. Просто запомни: один раз взял — один раз отдал.

std::mutex mtx;
std::vector<int> shared_vec;

void safe_push(int val) {
    std::lock_guard<std::mutex> lock(mtx); // RAII-захват
    shared_vec.push_back(val);
} // Автоматическая разблокировка при выходе из области видимости

Смотри, я тут сразу юзаю lock_guard — это чтобы не обосраться и не забыть разблокировать, если вылетит исключение. Умно, да?

Дальше идёт std::recursive_mutex. Вот это уже интереснее. Представь, у тебя функция рекурсивная, или там сложная логика, и нужно из одного потока несколько раз войти в критическую секцию. С обычным мьютексом — сам себя заблокируешь, будешь сидеть и ждать, пока сам же освободишь — волнение ебать, а результата ноль. А этот чувак позволяет одному потоку захватывать себя сколько угодно раз. Главное — отпустить столько же раз, сколько взял, а то опять заклинит.

std::recursive_mutex rec_mtx;
void recursive_func(int x) {
    std::lock_guard<std::recursive_mutex> lock(rec_mtx);
    if (x > 0) {
        recursive_func(x - 1); // Безопасный рекурсивный вызов
    }
}

Потом у нас std::timed_mutex. Это типа того же базового, но с таймером. Ну, знаешь, бывает, ждёшь ресурс, а он занят, и сидишь, бздишь, как дурак, не зная, сколько ждать. А тут можно сказать: "Э, дружок-пирожок, я тебя 100 миллисекунд подожду, а потом пошёл ты на хуй, буду другим делом заниматься". Методы try_lock_for() и try_lock_until() — твои лучшие друзья в таком случае.

std::timed_mutex tmtx;
if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {
    // Захватили за 100 мс
    tmtx.unlock();
} else {
    // Не удалось захватить, выполняем альтернативный код
}

Ну и логично, что есть std::recursive_timed_mutex. Это гибрид, ёб твою мать, два в одном: и рекурсивный, и с таймаутом. Для совсем изощрённых извращенцев.

А теперь, внимание, самый сочный — std::shared_mutex (появился в C++17). Это, блядь, просто песня! Паттерн "читатели-писатели" в чистом виде. Представь: у тесть куча потоков, которые только читают данные, и один, который иногда пишет. Зачем всем читателям ждать друг друга? Пусть читают одновременно, на здоровье! Но как только писатель приполз — всем читателям стоп, писатель делает своё грязное дело, а потом снова можно читать. Для чтения используется разделяемая блокировка (lock_shared()), а для записи — эксклюзивная (lock()).

std::shared_mutex sh_mtx;
std::string shared_data;

// Поток-читатель
{
    std::shared_lock lock(sh_mtx); // Разделяемая блокировка
    std::cout << shared_data;
}

// Поток-писатель
{
    std::unique_lock lock(sh_mtx); // Эксклюзивная блокировка
    shared_data = "new value";
}

И главный совет, который спасёт тебя от тысячи часов дебага: всегда, блядь, используй RAII-обёртки! std::lock_guard, std::unique_lock, std::shared_lock — это святое. Потому что если ты вручную lock() вызвал, а потом посередине твоего кода вылетело исключение — всё, пиши пропало, мьютекс повис намертво, deadlock, приехали. А эти штуки разблокируют всё автоматически, когда выходят из области видимости. Доверия ебать ноль к ручному управлению, только RAII. Запомни это как "Отче наш".