std::unique_lock vs std::lock_guard: что выбрать для мьютекса в C++?
Краткий ответ
Используйте 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?
- Условные переменные —
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 обязателен!
// Работа после сигнала
}
- Передача владения между функциями или потоками:
std::unique_lock<std::mutex> get_lock() {
std::unique_lock<std::mutex> lock(mtx);
// ... настройка ...
return lock; // move семантика
}
void process() {
auto lock = get_lock(); // Владение передано
// работа с данными
}
- 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 для ясности и производительности.
Ответов пока нет.
Войдите чтобы оставить ответ