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

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

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

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

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

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

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

Обзор механизмов синхронизации C++

С++11 стандартная библиотека предоставляет встроенные инструменты для многопоточного программирования, находящиеся в заголовке <thread>, <mutex>, <condition_variable>, <atomic> и др.

1. Mutex (Взаимное исключение)

std::mutex — базовый механизм для защиты доступа к общим ресурсам.

#include <mutex>
#include <thread>

std::mutex mtx;
int shared_counter = 0;

void increment() {
    mtx.lock();        // Занять мьютекс
    ++shared_counter;  // Критическая секция
    mtx.unlock();      // Освободить мьютекс
}

// Более безопасный способ: RAII
void increment_safe() {
    std::lock_guard<std::mutex> lock(mtx);  // Автоматический unlock
    ++shared_counter;
}  // unlock() вызывается автоматически при выходе из области видимости

// С C++17: std::scoped_lock (предпочтительно)
void increment_modern() {
    std::scoped_lock lock(mtx);
    ++shared_counter;
}

Типы мьютексов:

// std::mutex — обычный мьютекс
std::mutex mtx1;

// std::recursive_mutex — рекурсивный (одна нить может заблокировать несколько раз)
std::recursive_mutex mtx2;
void func(int depth) {
    std::lock_guard<std::recursive_mutex> lock(mtx2);
    if (depth > 0) func(depth - 1);  // OK: та же нить может заблокировать еще раз
}

// std::timed_mutex — с возможностью тайм-аута
std::timed_mutex mtx3;
if (mtx3.try_lock_for(std::chrono::milliseconds(100))) {
    // Удалось получить мьютекс за 100 мс
    mtx3.unlock();
} else {
    // Превышено время ожидания
}

// std::shared_mutex (C++17) — для читателей-писателей
std::shared_mutex mtx4;
int data = 0;

void reader_thread() {
    std::shared_lock<std::shared_mutex> lock(mtx4);  // Несколько читателей
    std::cout << data;  // Читаем
}

void writer_thread() {
    std::unique_lock<std::shared_mutex> lock(mtx4);  // Один писатель
    data++;  // Пишем
}

2. Lock Guards (RAII обертки)

// std::lock_guard — базовая обертка
{
    std::lock_guard<std::mutex> guard(mtx);
    // Автоматический lock/unlock
}  // unlock() здесь

// std::unique_lock — более гибкая обертка
{
    std::unique_lock<std::mutex> lock(mtx);
    // Можно разблокировать вручную
    lock.unlock();
    // И заблокировать снова
    lock.lock();
}  // unlock() при выходе, если еще заблокирован

// Отложенная блокировка
{
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);  // Не блокируем сразу
    // Делаем что-то
    lock.lock();  // Блокируем когда нужно
}

// std::scoped_lock (C++17) — для нескольких мьютексов
{
    std::mutex mtx1, mtx2, mtx3;
    std::scoped_lock locks(mtx1, mtx2, mtx3);  // Безопасно против deadlock
    // Все три мьютекса заблокированы
}

3. Condition Variable (Переменная условия)

Позволяет потокам ждать определенного условия.

#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void producer() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
        std::cout << "Producer: готово\n";
    }
    cv.notify_one();  // Пробудить один ждущий поток
    // или cv.notify_all();  // Пробудить все ждущие потоки
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    
    // Ждем пока ready станет true
    cv.wait(lock, [](){ return ready; });
    
    std::cout << "Consumer: начало работы\n";
}

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

Пример: Producer-Consumer очередь

template<typename T>
class ThreadSafeQueue {
private:
    mutable std::mutex mtx;
    std::condition_variable cv;
    std::queue<T> data;
    
public:
    void push(T value) {
        {
            std::lock_guard<std::mutex> lock(mtx);
            data.push(std::move(value));
        }
        cv.notify_one();
    }
    
    bool try_pop(T& value, std::chrono::milliseconds timeout) {
        std::unique_lock<std::mutex> lock(mtx);
        
        if (!cv.wait_for(lock, timeout, [this]{ return !data.empty(); })) {
            return false;  // timeout
        }
        
        value = std::move(data.front());
        data.pop();
        return true;
    }
};

4. Atomic (Атомарные операции)

Безопасные операции над общими переменными без явных мьютексов.

#include <atomic>

std::atomic<int> counter(0);  // Инициализация

void increment_atomic() {
    counter++;  // Атомарное увеличение
    counter.store(5, std::memory_order_relaxed);  // Запись
    int val = counter.load(std::memory_order_acquire);  // Чтение
}

// Compare and swap
int expected = 5;
if (counter.compare_exchange_strong(expected, 10)) {
    std::cout << "Успешно изменили с 5 на 10\n";
} else {
    std::cout << "Значение было " << expected << ", не 5\n";
}

// Memory ordering
std::atomic<bool> flag(false);

void thread1() {
    flag.store(true, std::memory_order_release);  // Освобождаем ресурсы
}

void thread2() {
    while (!flag.load(std::memory_order_acquire)) {}  // Ждем
    // Здесь все операции thread1 были завершены
}

Memory ordering (порядок памяти):

// От слабого к сильному:
std::memory_order_relaxed;      // Нет синхронизации
std::memory_order_consume;      // Есть зависимость данных
std::memory_order_acquire;      // acquire семантика
std::memory_order_release;      // release семантика
std::memory_order_acq_rel;      // acquire + release
std::memory_order_seq_cst;      // Полная синхронизация (по умолчанию)

// Пример: программа с гарантией упорядочивания
std::atomic<int> x(0), y(0);

void writer() {
    x.store(1, std::memory_order_release);
    y.store(1, std::memory_order_release);
}

void reader() {
    while (y.load(std::memory_order_acquire) == 0) {}
    // Гарантированно x уже = 1
    std::cout << x.load();  // Выведет 1
}

5. std::call_once и std::once_flag

Для инициализации ровно один раз в многопоточном контексте.

#include <mutex>

std::once_flag init_flag;
int initialized = 0;

void init_function() {
    initialized = 1;
    std::cout << "Инициализация выполнена\n";
}

void thread_function(int id) {
    std::call_once(init_flag, init_function);  // Вызовется ровно один раз
    std::cout << "Поток " << id << " работает\n";
}

int main() {
    std::thread t1(thread_function, 1);
    std::thread t2(thread_function, 2);
    std::thread t3(thread_function, 3);
    
    t1.join(); t2.join(); t3.join();
    // Вывод:
    // Инициализация выполнена (ровно один раз)
    // Поток 1 работает
    // Поток 2 работает
    // Поток 3 работает
}

6. Barrier, Latch (C++20)

#include <barrier>

std::barrier sync_point(3);  // Ждем 3 потока

void worker(int id) {
    std::cout << "Поток " << id << " начал\n";
    
    sync_point.arrive_and_wait();  // Точка синхронизации
    
    std::cout << "Поток " << id << " после барьера\n";
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    std::thread t3(worker, 3);
    
    t1.join(); t2.join(); t3.join();
}

7. Практический пример: Потокобезопасный синглтон

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;
    
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
public:
    static Singleton* getInstance() {
        std::lock_guard<std::mutex> lock(mtx);
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
    
    // Или более эффективно с call_once:
    static Singleton* getInstancev2() {
        static std::once_flag flag;
        std::call_once(flag, []() { instance = new Singleton(); });
        return instance;
    }
    
    // Или самый простой вариант (C++11):
    static Singleton& getInstancev3() {
        static Singleton instance;
        return instance;
    }
};

8. Сравнение механизмов

МеханизмНазначениеПроизводительность
std::mutexЗащита критической секцииСредняя
std::atomicПростые атомарные операцииВысокая
std::condition_variableСинхронизация между потокамиСредняя
std::lock_guardRAII обертка для mutexНулевая стоимость абстракции
std::call_onceИнициализация один разНизкая (выполняется один раз)

Лучшие практики

  1. Используй RAII: всегда lock_guard или unique_lock вместо lock/unlock
  2. Избегай deadlock: используй std::scoped_lock для нескольких мьютексов
  3. Выбирай правильный механизм: atomic для счетчиков, mutex для сложных структур
  4. Минимизируй критическую секцию: занимай мьютекс минимально
  5. Помни о memory_order: используй relaxed для счетчиков, seq_cst когда не уверен

Заключение

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