Avatar ·

std::unique_lock vs std::lock_guard: что выбрать для мьютекса в C++?

Привет! Я начал изучать многопоточность в C++11 и запутался в выборе между std::unique_lock и std::lock_guard. Оба класса используются для блокировки мьютекса, но я не понимаю, в каких случаях какой применять. Например, я пишу простой потокобезопасный счётчик: ```cpp #include #include #include std::mutex mtx; int counter = 0; void increment() { for (int i = 0; i < 100000; ++i) { // Что здесь лучше? std::lock_guard lock(mtx); // или std::unique_lock lock(mtx); ++counter; } } ``` Оба варианта компилируются и работают. Но я слышал, что unique_lock "тяжелее". Когда его стоит использовать? Есть ли ситуации, где lock_guard не подходит? И почему в стандартной библиотеке есть два похожих класса? Объясните, пожалуйста, на примерах. Заранее спасибо за помощь! <<>>

Краткий ответ

Используйте std::lock_guard по умолчанию. Он легче, проще и безопаснее для 95% случаев. std::unique_lock — это "швейцарский нож" для сложных сценариев, когда нужен гибкий контроль над блокировкой.

Подробное сравнение

Оба класса реализуют RAII (Resource Acquisition Is Initialization): они захватывают мьютекс в конструкторе и автоматически освобождают в деструкторе. Главное отличие — гибкость.

std::lock_guard

  • Простой и минималистичный: только захват и освобождение.
  • Нельзя отложить блокировку, разблокировать раньше времени или передать владение.
  • Идеален для коротких критических секций.
void safe_increment() {
    std::lock_guard<std::mutex> lock(mtx); // Захват
    ++counter;                              // Критическая секция
}                                           // Автоматическое освобождение

std::unique_lock

  • Поддерживает отложенный захват (defer_lock), попытку захвата (try_lock), временное освобождение (unlock/lock).
  • Может передавать владение мьютексом (move semantics).
  • Работает с условными переменными (std::condition_variable).
void flexible_lock() {
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // Не захватываем сразу
    // ... подготовка ...
    lock.lock();  // Ручной захват
    // критическая секция
    lock.unlock(); // Раннее освобождение
    // ... работа без блокировки ...
    lock.lock();   // Повторный захват
}

Когда использовать unique_lock?

  1. Условные переменные — std::condition_variable::wait() требует unique_lock:
std::condition_variable cv;
std::mutex cv_mtx;
bool ready = false;

void worker() {
    std::unique_lock<std::mutex> lock(cv_mtx);
    cv.wait(lock, []{ return ready; }); // unique_lock обязателен!
    // Работа после сигнала
}
  1. Передача владения между функциями или потоками:
std::unique_lock<std::mutex> get_lock() {
    std::unique_lock<std::mutex> lock(mtx);
    // ... настройка ...
    return lock; // move семантика
}

void process() {
    auto lock = get_lock(); // Владение передано
    // работа с данными
}
  1. try_lock для неблокирующих попыток:
void try_increment() {
    std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
    if (lock.owns_lock()) {
        ++counter; // Безопасно
    } else {
        // Мьютекс занят — делаем что-то другое
    }
}

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

unique_lock хранит флаг владения и поддерживает дополнительные операции, что даёт небольшой оверхед (обычно ~8 байт памяти + несколько инструкций). Для 99% приложений разница незаметна. Но в высоконагруженных циклах с микросекундными блокировками lock_guard будет чуть быстрее.

Золотое правило

Начинайте с lock_guard. Если позже понадобится дополнительная гибкость — замените на unique_lock (это тривиально, так как интерфейсы похожи). Не усложняйте код без необходимости.

// Хороший рефакторинг: начали с lock_guard
void process_data() {
    std::lock_guard<std::mutex> lock(mtx);
    // ... логика ...
}

// Понадобилась условная переменная — меняем на unique_lock
void process_data_with_cv() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return data_ready; });
    // ... логика ...
}
<<>>

Альтернативный взгляд: когда unique_lock — единственный выбор

Дополню коллегу: помимо условных переменных, std::unique_lock незаменим при работе с несколькими мьютексами и deadlock-безопасными алгоритмами.

Пример: блокировка нескольких мьютексов

std::mutex mtx1, mtx2;

void transfer() {
    std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
    std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
    std::lock(lock1, lock2); // Безопасное одновременное захватывание
    // работа с обоими ресурсами
}

std::lock() принимает только unique_lock (или lock_guard не подходит), так как ему нужно отложить блокировку и управлять порядком захвата для избежания взаимоблокировок.

Совет

Если видите, что в коде приходится вызывать std::lock() для нескольких мьютексов — смело используйте unique_lock. В остальных случаях придерживайтесь lock_guard для ясности и производительности.