Какие знаешь механизмы синхронизации в стандартной библиотеке?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Какие знаешь механизмы синхронизации в стандартной библиотеке?
Обзор механизмов синхронизации 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_guard | RAII обертка для mutex | Нулевая стоимость абстракции |
| std::call_once | Инициализация один раз | Низкая (выполняется один раз) |
Лучшие практики
- Используй RAII: всегда lock_guard или unique_lock вместо lock/unlock
- Избегай deadlock: используй std::scoped_lock для нескольких мьютексов
- Выбирай правильный механизм: atomic для счетчиков, mutex для сложных структур
- Минимизируй критическую секцию: занимай мьютекс минимально
- Помни о memory_order: используй relaxed для счетчиков, seq_cst когда не уверен
Заключение
C++11 предоставляет полный набор инструментов для многопоточного программирования: от базовых мьютексов до продвинутых операций с памятью. Правильный выбор механизма критичен для производительности и корректности параллельных программ.