concurrency — фундаментconcurrency — fundamentals

Примитивы синхронизацииSynchronization Primitives

Мьютекс решает ровно одну задачу — взаимное исключение. Всё остальное семейство (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.

C++11C++11 частый вопросcommon interview question

std::mutex

Мьютекс (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();
}

lock_guard иand unique_lock

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).

std::condition_variable

Мьютекс сам по себе не даёт способа потоку эффективно "подождать, пока что-то произойдёт" — только опрашивать флаг в цикле (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.

ГраблиGotchas

Deadlock при захвате двух мьютексов в разном порядкеDeadlock from acquiring two mutexes in different order

Поток 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.

Мьютекс не является recursive по умолчаниюA mutex isn't recursive by default

Повторный вызов 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).

Держать мьютекс дольше необходимого — источник искусственной сериализацииHolding a mutex longer than necessary causes artificial serialization

Дорогие вычисления (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.