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

Какие знаешь Lock Guard в стандартной библиотеке?

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

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

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

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

Lock Guard в стандартной библиотеке C++

Что такое Lock Guard

Lock Guard — это шаблоны классов из <mutex>, которые реализуют RAII-паттерн (Resource Acquisition Is Initialization) для управления блокировками. Они автоматически захватывают мьютекс при создании и освобождают при уничтожении.

1. std::lock_guard (C++11)

Самый простой и часто используемый lock guard. Не позволяет переносить или копировать блокировку.

#include <mutex>
#include <iostream>
#include <thread>

std::mutex m;
int counter = 0;

void incrementCounter() {
    // std::lock_guard захватывает мьютекс при конструировании
    std::lock_guard<std::mutex> lock(m);
    
    // Критическая секция
    counter++;
    std::cout << "Счётчик: " << counter << std::endl;
    
    // Мьютекс автоматически освобождается когда lock выходит из области видимости
}  // lock_guard деструктор: unlock()

int main() {
    std::thread t1(incrementCounter);
    std::thread t2(incrementCounter);
    
    t1.join();
    t2.join();
    
    return 0;
}

Преимущества std::lock_guard

// ✅ Безопасность при исключениях
std::lock_guard<std::mutex> lock(m);
try {
    dangerousOperation();  // Может выбросить исключение
}  // Мьютекс всё равно будет освобождён

// ❌ Если бы мы делали это без lock_guard:
std::mutex::lock();
try {
    dangerousOperation();  // Выброшено исключение
}  // Мьютекс НИКОГДА не будет разблокирован!
std::mutex::unlock();

2. std::unique_lock (C++11)

Более гибкий lock guard с дополнительными возможностями:

#include <mutex>
#include <iostream>

std::mutex m;
int value = 0;

void example1() {
    // Стандартное использование (как lock_guard)
    std::unique_lock<std::mutex> lock(m);
    value = 10;
}  // Разблокировка

void example2() {
    // Отложенный захват
    std::unique_lock<std::mutex> lock(m, std::defer_lock);
    // Сейчас мьютекс НЕ захвачен
    
    std::cout << "Перед захватом" << std::endl;
    lock.lock();  // Явный захват
    std::cout << "Во время захвата" << std::endl;
    lock.unlock();  // Явное освобождение
    std::cout << "После освобождения" << std::endl;
}  // Деструктор вызовется, но мьютекс уже разблокирован

void example3() {
    // Пробование захватить мьютекс с таймаутом
    std::unique_lock<std::timed_mutex> lock(m);
    // Используется timed_mutex вместо обычного mutex
}

void example4() {
    // Условная переменная работает с unique_lock
    std::condition_variable cv;
    std::unique_lock<std::mutex> lock(m);
    cv.wait(lock, []() { return value > 0; });
    // Внутри wait() мьютекс временно освобождается
}

int main() {
    example1();
    example2();
    return 0;
}

Конструкторы std::unique_lock

КонструкторОписание
unique_lock(m)Захватывает мьютекс сразу
unique_lock(m, defer_lock)НЕ захватывает мьютекс
unique_lock(m, try_to_lock)Пробует захватить без блокирования
unique_lock(m, adopt_lock)Принимает уже захваченный мьютекс

Методы std::unique_lock

std::unique_lock<std::mutex> lock(m, std::defer_lock);

// Захват и освобождение
lock.lock();        // Захватить
lock.unlock();      // Освободить
bool ok = lock.try_lock();  // Попробовать захватить без ожидания

// Проверка состояния
if (lock.owns_lock()) {     // Захватил ли мьютекс?
    std::cout << "Владею мьютексом" << std::endl;
}

// Передача владения (move semantics)
std::unique_lock<std::mutex> lock2 = std::move(lock);
// Теперь lock2 владеет мьютексом, а lock больше не владеет

3. std::shared_lock (C++14)

Для ситуаций, когда несколько потоков могут одновременно читать, но только один может писать:

#include <shared_mutex>
#include <iostream>
#include <thread>
#include <vector>

class Database {
private:
    std::shared_mutex mtx;
    std::vector<int> data;
    
public:
    // Чтение: несколько потоков одновременно
    int read(int index) const {
        std::shared_lock<std::shared_mutex> lock(mtx);
        return data[index];
    }
    
    // Запись: только один поток
    void write(int index, int value) {
        std::unique_lock<std::shared_mutex> lock(mtx);
        data[index] = value;
    }
};

int main() {
    Database db;
    
    // Много потоков могут читать одновременно
    std::thread reader1([&db]() { std::cout << db.read(0) << std::endl; });
    std::thread reader2([&db]() { std::cout << db.read(0) << std::endl; });
    
    // Писать может только один поток
    std::thread writer([&db]() { db.write(0, 42); });
    
    reader1.join();
    reader2.join();
    writer.join();
    
    return 0;
}

4. std::scoped_lock (C++17)

Модернизированная версия для захвата нескольких мьютексов без дедлока:

#include <mutex>

std::mutex m1, m2, m3;

// Старый способ (опасен для дедлока)
void oldWay() {
    std::lock_guard<std::mutex> lock1(m1);
    std::lock_guard<std::mutex> lock2(m2);  // Может дедлокнуться
}

// Новый способ (безопасен от дедлока)
void newWay() {
    // Автоматически захватывает все мьютексы в правильном порядке
    std::scoped_lock lock(m1, m2, m3);
    
    // Все три мьютекса захвачены, и дедлока не будет
}  // Все три мьютекса освобождаются

Сравнение Lock Guard типов

КлассМожно освобождатьМожно передаватьУсловные переменныеНескольким мьютексамКогда использовать
lock_guardНетНетНетОдинПростые случаи
unique_lockДаДа (move)ДаОдинГибкие сценарии
shared_lockНетНетНетОдин (shared)Читатели-писатели
scoped_lockНетНетНетНесколькоНесколько мьютексов

Практический пример: Безопасный счётчик

#include <mutex>
#include <iostream>
#include <thread>
#include <vector>

class ThreadSafeCounter {
private:
    mutable std::mutex mtx;
    int value = 0;
    
public:
    void increment() {
        std::lock_guard<std::mutex> lock(mtx);
        value++;
    }
    
    int getValue() const {
        std::lock_guard<std::mutex> lock(mtx);
        return value;
    }
};

int main() {
    ThreadSafeCounter counter;
    std::vector<std::thread> threads;
    
    // Создаём 10 потоков, каждый инкрементирует счётчик 1000 раз
    for (int i = 0; i < 10; i++) {
        threads.emplace_back([&counter]() {
            for (int j = 0; j < 1000; j++) {
                counter.increment();
            }
        });
    }
    
    // Ждём все потоки
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "Финальное значение: " << counter.getValue() << std::endl;  // 10000
    return 0;
}

Практический пример: Читатели-писатели

#include <shared_mutex>
#include <string>
#include <iostream>

class SharedData {
private:
    mutable std::shared_mutex mtx;
    std::string data;
    
public:
    // Много потоков могут читать одновременно
    std::string read() const {
        std::shared_lock<std::shared_mutex> lock(mtx);
        return data;
    }
    
    // Только один поток может писать
    void write(const std::string& newData) {
        std::unique_lock<std::shared_mutex> lock(mtx);
        data = newData;
    }
};

Ключевые моменты

  • std::lock_guard — самый простой и быстрый для базовых случаев
  • std::unique_lock — универсальный с дополнительными возможностями
  • std::shared_lock — для паттерна читателей-писателей
  • std::scoped_lock — для нескольких мьютексов без дедлока
  • RAII гарантирует, что мьютекс освобождается в любом случае
  • Lock Guard запрещает копирование, но unique_lock позволяет перемещение
Какие знаешь Lock Guard в стандартной библиотеке? | PrepBro