Мутексы, замки и критические секции
Основные примитивы синхронизации: mutex, lock guard, unique lock, scoped lock, shared mutex. Научитесь защищать общие данные, предотвращать тупики и понимать разницу между расами данных и условиями гонки.
Почему важна синхронизация
Когда несколько потоков получают доступ к одной и той же памяти и, по крайней мере, один из них пишет, у вас есть раса данных - форма неопределенного поведения ** в C++. Стандарт не просто говорит, что «результат непредсказуем». В нем говорится, что может произойти все: поврежденные данные, сбои, уязвимости безопасности или, казалось бы, правильное поведение, которое нарушается при изменении флагов компилятора.
mutex (взаимное исключение) — фундаментальный примитив синхронизации. Только одна нить может одновременно удерживать замок мутекса. Любая другая нить, которая пытается заблокировать его, будет блокироваться до тех пор, пока mutex не будет выпущен. Защищая общие данные с помощью mutex, вы сериализуете доступ и устраняете гонки данных.
Обсуждение:mutex and lock guard
Самый простой и безопасный образец — std::lock_guard — обертка RAII, которая запирает мутекс на конструкцию и разблокирует его на разрушение, даже если забрасывается исключение.
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx;
int shared_counter = 0;
void increment(int times) {
for (int i = 0; i < times; ++i) {
std::lock_guard<std::mutex> lock(mtx); // RAII lock
++shared_counter; // safe: only one thread at a time
} // lock released here automatically
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment, 10000);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Counter: " << shared_counter << "\n";
// Always prints 100000 — no data race
return 0;
}Unique lock - Гибкая блокировка
std::unique_lock более гибкий, чем lock_guard. Он поддерживает отложенную блокировку, временную блокировку, ручную блокировку / разблокировку и требуется std::condition_variable.
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtx;
void try_lock_example() {
// Deferred locking — don't lock yet
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// Try to lock with a timeout
if (lock.try_lock_for(std::chrono::milliseconds(100))) {
std::cout << "Acquired lock\n";
// ... do work ...
} else {
std::cout << "Could not acquire lock within 100ms\n";
}
// Automatically unlocks when lock goes out of scope
}
void manual_control() {
std::unique_lock<std::mutex> lock(mtx);
// ... critical section ...
lock.unlock(); // release early if needed
// ... non-critical work ...
lock.lock(); // re-acquire
// ... another critical section ...
}scoped lock — Deadlock-Free Multi-Lock (C++17)
При одновременной блокировке нескольких мутексов std::scoped_lock (C++17) использует внутренний алгоритм избежания тупика. Он заменяет подверженное ошибкам ручное использование std::lock.
#include <mutex>
#include <thread>
#include <iostream>
struct BankAccount {
std::mutex mtx;
double balance = 1000.0;
};
void transfer(BankAccount& from, BankAccount& to, double amount) {
// Lock both mutexes without risk of deadlock
std::scoped_lock lock(from.mtx, to.mtx);
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
std::cout << "Transferred " << amount << "\n";
}
} // both mutexes released
int main() {
BankAccount alice, bob;
// These two transfers lock accounts in different order,
// but scoped_lock prevents deadlock.
std::thread t1(transfer, std::ref(alice), std::ref(bob), 100.0);
std::thread t2(transfer, std::ref(bob), std::ref(alice), 50.0);
t1.join();
t2.join();
std::cout << "Alice: " << alice.balance
<< ", Bob: " << bob.balance << "\n";
return 0;
}shared mutex — Reader-Writer Lock (C++17)
При чтении значительно больше, чем пишет, std::shared_mutex позволяет нескольким одновременным читателям , но эксклюзивный доступ к автору . Используйте std::shared_lock для читателей и std::unique_lock для писателей.
#include <shared_mutex>
#include <mutex>
#include <thread>
#include <iostream>
#include <map>
#include <string>
class ThreadSafeCache {
mutable std::shared_mutex smtx_;
std::map<std::string, int> data_;
public:
int read(const std::string& key) const {
std::shared_lock lock(smtx_); // multiple readers OK
auto it = data_.find(key);
return it != data_.end() ? it->second : -1;
}
void write(const std::string& key, int value) {
std::unique_lock lock(smtx_); // exclusive access
data_[key] = value;
}
};Гонка данных происходит, когда два потока одновременно получают доступ к одному и тому же местоположению памяти и, по крайней мере, один записывает без синхронизации. В C++ это неопределенное поведение — не просто «недетерминированный выход», но что-то может произойти UB.
Условие гонки - это логическая ошибка более высокого уровня, где правильность программы зависит от времени. Даже код, свободный от рас данных, может иметь условия гонки (например, проверить, а затем действовать на параллельной карте). Мутексы устраняют гонки данных; устранение расовых условий требует тщательного проектирования ваших параллельных алгоритмов.
Всегда используйте обертки блокировки RAII (lock_guard, scoped_lock, unique_lock) вместо необработанных вызовов mutex::lock()/unlock(). Держите критические разделы как можно короче — никогда не выполняйте ввод/вывод, распределение или блокировку звонков, удерживая замок, если вы можете избежать этого. Когда вам нужно несколько мутексов, используйте std::scoped_lock, чтобы избежать тупиков. Если вы должны блокировать вручную, всегда блокируйте в соответствии с глобальным порядком.
- Раса данных — это неопределенное поведение — используйте mutex для защиты каждой совместно используемой переменной.
std::lock_guard- самый простой замок RAII; предпочтите его для простых критических секций.std::unique_lockдобавляет гибкость: отложенная блокировка, временная блокировка и ручная блокировка / разблокировкаstd::scoped_lock(C++17) блокирует несколько мутексов одновременно без тупикаstd::shared_mutex(C++17) позволяет нескольким одновременным читателям, но эксклюзивным авторам- Держите критические секции короткими и никогда не держите замок, ожидая другой нити.
Тест — проверьте свои знания
(20 XP)1. В чем главное преимущество `std::scoped_lock` перед ручным вызовом `lock()` на нескольких мутексах?
2. Какой тип блокировки использовать с `std::condition_variable`?
3. В C++, что является следствием гонки данных?