concurrency — фундаментconcurrency — fundamentals
Мьютекс решает ровно одну задачу — взаимное исключение. Всё остальное семейство (unique_lock, lock_guard, condition_variable) — это RAII-обёртки и механизмы ожидания поверх этой одной идеи, каждый со своим компромиссом между гибкостью и простотой.A mutex solves exactly one problem — mutual exclusion. The rest of the family (unique_lock, lock_guard, condition_variable) are RAII wrappers and waiting mechanisms built on top of that one idea, each with its own trade-off between flexibility and simplicity.
Мьютекс (mutual exclusion) гарантирует, что критическую секцию — код между lock() и unlock() — выполняет не более одного потока одновременно; остальные потоки, вызвавшие lock(), блокируются до освобождения. Голый вызов lock()/unlock() вручную — антипаттерн: любое исключение или ранний return между ними оставляет мьютекс заблокированным навсегда (deadlock для всех, кто ждёт).A mutex (mutual exclusion) guarantees that no more than one thread at a time executes the critical section — the code between lock() and unlock(); other threads calling lock() block until it's released. Calling lock()/unlock() manually and raw is an anti-pattern: any exception or early return between them leaves the mutex locked forever (a deadlock for everyone waiting).
std::mutex m;
int shared_counter = 0;
void increment() {
m.lock();
++shared_counter;
if (shared_counter > 100) throw std::runtime_error("overflow"); // мьютекс НЕ разблокируется — deadlock// mutex is NEVER unlocked — deadlock
m.unlock();
}
std::lock_guard — минимальная RAII-обёртка: захватывает мьютекс в конструкторе, освобождает в деструкторе, ничего больше не умеет (нет ручной разблокировки, нет move). std::unique_lock — более тяжёлая, но гибкая обёртка: поддерживает отложенный захват (std::defer_lock), ручную разблокировку/повторный захват внутри своего времени жизни, move-семантику, и — критично — это единственный из двух, который можно передать в std::condition_variable::wait(), потому что wait должен уметь временно разблокировать мьютекс.std::lock_guard is a minimal RAII wrapper: acquires the mutex in its constructor, releases it in the destructor, and does nothing else (no manual unlock, no move). std::unique_lock is a heavier but more flexible wrapper: it supports deferred locking (std::defer_lock), manual unlock/relock within its lifetime, move semantics, and — critically — it's the only one of the two that can be passed to std::condition_variable::wait(), because wait needs to be able to temporarily unlock the mutex.
void increment() {
std::lock_guard<std::mutex> lock(m); // захват здесь// acquired here
++shared_counter;
if (shared_counter > 100) throw std::runtime_error("overflow"); // деструктор lock отработает при размотке стека — мьютекс освобождён// lock's destructor runs during stack unwinding — mutex released
} // или здесь, при обычном выходе// or here, on normal exit
void tryTwoMutexes(std::mutex& a, std::mutex& b) {
std::unique_lock<std::mutex> lockA(a, std::defer_lock);
std::unique_lock<std::mutex> lockB(b, std::defer_lock);
std::lock(lockA, lockB); // захват обоих сразу, без риска deadlock из-за порядка// locks both at once, no deadlock risk from ordering
}
Общее правило выбора: lock_guard по умолчанию (дешевле, проще, невозможно использовать неправильно), unique_lock — только когда реально нужна его дополнительная гибкость (condition_variable, ручное управление временем удержания блокировки, отложенный/множественный захват).The general rule: lock_guard by default (cheaper, simpler, hard to misuse), unique_lock only when its extra flexibility is genuinely needed (condition_variable, manual control over how long the lock is held, deferred/multi-mutex locking).
Мьютекс сам по себе не даёт способа потоку эффективно "подождать, пока что-то произойдёт" — только опрашивать флаг в цикле (busy-wait, тратит CPU впустую). condition_variable позволяет потоку заснуть, атомарно освободив мьютекс, и проснуться, когда другой поток вызовет notify_one()/notify_all(), повторно захватив мьютекс перед продолжением.A mutex by itself gives no way for a thread to efficiently "wait until something happens" — only to poll a flag in a loop (busy-waiting, wasting CPU). condition_variable lets a thread sleep, atomically releasing the mutex, and wake up when another thread calls notify_one()/notify_all(), reacquiring the mutex before continuing.
std::mutex m;
std::condition_variable cv;
std::queue<int> q;
bool done = false;
void producer() {
{
std::lock_guard<std::mutex> lock(m);
q.push(42);
}
cv.notify_one(); // разбудить одного ждущего потребителя// wake one waiting consumer
}
void consumer() {
std::unique_lock<std::mutex> lock(m); // именно unique_lock — cv.wait разблокирует его внутри// must be unique_lock — cv.wait unlocks it internally
cv.wait(lock, [] { return !q.empty(); }); // предикат защищает от spurious wakeup и потерянных notify// the predicate guards against spurious wakeup and lost notifies
int value = q.front(); q.pop();
}
Предикат в wait(lock, predicate) — не стилистическая рекомендация, а необходимость: condition_variable формально разрешено просыпаться "спонтанно" без единого вызова notify (spurious wakeup), и если notify_one() вызывается до того, как поток дошёл до wait(), само уведомление теряется безвозвратно. Проверка предиката в цикле (что и делает эта форма `wait`) защищает от обоих случаев.The predicate in wait(lock, predicate) isn't a stylistic suggestion — it's a necessity: condition_variable is formally allowed to wake up "spuriously" with no notify call at all, and if notify_one() is called before the thread reaches wait(), that notification is lost for good. Checking the predicate in a loop (which is exactly what this form of `wait` does) guards against both cases.
Поток A захватывает mutex1, затем ждёт mutex2; поток B захватывает mutex2, затем ждёт mutex1 — классический deadlock. Решения: единый глобальный порядок захвата для всех мьютексов в кодовой базе, либо std::lock()/std::scoped_lock (C++17), захватывающие несколько мьютексов атомарно без риска такого взаимного ожидания.Thread A locks mutex1, then waits for mutex2; thread B locks mutex2, then waits for mutex1 — the classic deadlock. Fixes: a single global lock-ordering convention across the codebase, or std::lock()/std::scoped_lock (C++17), which acquire multiple mutexes atomically with no risk of this mutual wait.
Повторный вызов lock() тем же потоком, уже держащим std::mutex (например, при рекурсивном вызове функции, содержащей блокировку) — undefined behavior, чаще всего deadlock самого себя. std::recursive_mutex решает это специально, но обычно сигнализирует о проблеме дизайна (критическая секция слишком широка или структурирована неудачно).Calling lock() again from the same thread that already holds a std::mutex (e.g., during a recursive call into a function that locks) is undefined behavior, most commonly a self-deadlock. std::recursive_mutex solves this specifically, but usually signals a design problem (the critical section is too broad or poorly structured).
Дорогие вычисления (I/O, аллокации, тяжёлая математика) внутри критической секции сериализуют все потоки без необходимости — критическая секция должна защищать только реальный доступ к разделяемым данным, а не любую логику, которая с ними соприкасается.Expensive computation (I/O, allocation, heavy math) inside a critical section needlessly serializes every thread — a critical section should protect only actual access to shared data, not any logic that happens to touch it.