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

Какие знаешь виды мьютексов из стандартной библиотеки?

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

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

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

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

Виды мьютексов в стандартной библиотеке C++

Мьютексы — это примитивы синхронизации для контроля доступа к общим ресурсам в многопоточной программе. C++ предоставляет несколько типов с разными свойствами и производительностью.

1. std::mutex (базовый мьютекс)

Описание: Базовый, не рекурсивный мьютекс.

#include <mutex>

std::mutex mtx;

// Захват и освобождение
mtx.lock();
// критическая секция
mtx.unlock();

// Или безопаснее с RAII:
{
    std::lock_guard<std::mutex> lock(mtx);
    // мьютекс автоматически освобождается
}

Свойства:

  • Не рекурсивный (один поток не может заблокировать дважды)
  • Простой и быстрый
  • Не полезен для заданной временной длительности

Когда использовать: Базовый случай защиты общего ресурса между потоками.

2. std::recursive_mutex (рекурсивный мьютекс)

Описание: Позволяет одному потоку захватить мьютекс несколько раз.

std::recursive_mutex rmtx;

void function_a() {
    std::lock_guard<std::recursive_mutex> lock(rmtx);
    // делаем что-то
}

void function_b() {
    std::lock_guard<std::recursive_mutex> lock(rmtx);
    function_a();  // Рекурсивный вызов работает!
}

Сложность: Требует подсчёта глубины блокировки:

Поток T1:
  lock()     -> count = 1
  lock()     -> count = 2
  lock()     -> count = 3
  unlock()   -> count = 2
  unlock()   -> count = 1
  unlock()   -> count = 0 (полностью разблокирован)

Когда использовать: Когда функция может вызывать другую функцию, которая требует того же мьютекса.

Важно: Избегай рекурсивных мьютексов в production — часто признак плохого дизайна. Лучше переструктурировать код.

3. std::timed_mutex (мьютекс с timeout)

Описание: Базовый мьютекс с поддержкой timeout'а при захвате.

#include <mutex>

std::timed_mutex tmtx;

// Попытка захватить с timeout
if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {
    // Захватили в течение 100мс
    // ...
    tmtx.unlock();
} else {
    // Не удалось захватить за 100мс
    std::cout << "Timeout!" << std::endl;
}

// Или с абсолютным временем
std::chrono::steady_clock::time_point deadline = 
    std::chrono::steady_clock::now() + std::chrono::seconds(1);

if (tmtx.try_lock_until(deadline)) {
    // ...
    tmtx.unlock();
}

Свойства:

  • try_lock_for(duration) — ждёт относительное время
  • try_lock_until(time_point) — ждёт до абсолютного времени
  • Медленнее обычного mutex из-за работы с временем

4. std::recursive_timed_mutex

Описание: Комбинация рекурсивного и таймирующего мьютекса.

std::recursive_timed_mutex rtmtx;

if (rtmtx.try_lock_for(std::chrono::milliseconds(100))) {
    // Рекурсивный вызов с timeout'ом
    rtmtx.lock();  // Второй захват
    // ...
    rtmtx.unlock();
    rtmtx.unlock();
} else {
    std::cout << "Timeout!" << std::endl;
}

Свойства:

  • Самый универсальный, но и самый медленный
  • Используется редко — обычно признак плохого дизайна

5. std::shared_mutex (читатель-писатель мьютекс) — C++17

Описание: Позволяет несколько читателей ИЛИ одного писателя одновременно.

#include <shared_mutex>

std::shared_mutex data_mtx;
int shared_data = 0;

// Читатель
void reader() {
    std::shared_lock<std::shared_mutex> lock(data_mtx);
    std::cout << shared_data;  // Многие читатели могут одновременно
}

// Писатель
void writer(int value) {
    std::unique_lock<std::shared_mutex> lock(data_mtx);
    shared_data = value;  // Только один писатель, читатели ждут
}

Сценарий:

Время:  R1    R2    R3    W1    R4    R5
        |-----|-----|-----|     |-----|-----|

Читатели 1,2,3 могут работать одновременно (R1, R2, R3)
Писатель W1 ждёт, пока они закончат
Когда W1 пишет, читатели ждут
После W1, читатели 4,5 могут работать

Производительность:

  • Для read-heavy workload: намного быстрее, чем обычный mutex
  • Для write-heavy workload: медленнее из-за overhead'а

6. std::shared_timed_mutex — C++14

Описание: Комбинация shared_mutex с поддержкой timeout.

std::shared_timed_mutex stmtx;

// Читатель с timeout
if (stmtx.try_lock_shared_for(std::chrono::milliseconds(100))) {
    // Что-то читаем
    stmtx.unlock_shared();
}

// Писатель с timeout
if (stmtx.try_lock_for(std::chrono::milliseconds(100))) {
    // Что-то пишем
    stmtx.unlock();
}

Сравнение всех видов

МьютексРекурсивныйTimeoutЧит/ПисательСкоростьЮз-кейс
mutexНетНетНетБыстроОбщие ресурсы
recursive_mutexДаНетНетСреднеРекурсивный доступ
timed_mutexНетДаНетСреднеТайм-ауты
recursive_timed_mutexДаДаНетМедленноРедко
shared_mutexНетНетДаБыстро*Read-heavy данные
shared_timed_mutexНетДаДаСреднеRead-heavy + timeout

*Быстрее для читателей, но медленнее для писателей

Best Practices

1. Используй std::lock_guard или std::unique_lock (RAII)

// ПРАВИЛЬНО - автоматическое освобождение
{
    std::lock_guard<std::mutex> lock(mtx);
    // критическая секция
} // lock автоматически вызывает unlock()

// НЕПРАВИЛЬНО - может забыть unlock()
mtx.lock();
// если есть исключение, unlock() не будет вызван
mtx.unlock();

2. Минимизируй время в критической секции

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

// НЕПРАВИЛЬНО - долгая операция под мьютексом
{
    std::lock_guard<std::mutex> lock(mtx);
    auto item = data[0];
    heavy_computation(item);  // Долго!
}

// ПРАВИЛЬНО - только защищаем доступ к данным
int item;
{
    std::lock_guard<std::mutex> lock(mtx);
    item = data[0];  // Быстро
}
heavy_computation(item);  // Снаружи мьютекса

3. Для read-heavy workload используй shared_mutex

std::shared_mutex config_mtx;
std::string config_value;

// Множество потоков могут читать одновременно
std::shared_lock<std::shared_mutex> lock(config_mtx);
std::cout << config_value;

4. Избегай рекурсивных мьютексов

Они медленнее и часто признак плохого дизайна. Лучше переструктурировать код.

Рекомендация для production

Для большинства backend-приложений:

  1. std::mutex — базовый выбор (95% случаев)
  2. std::shared_mutex — если много читателей
  3. Избегай тайм-аутов и рекурсии — обычно признак проблем дизайна
  4. Всегда используй lock_guard/unique_lock (RAII)