многопоточностьconcurrency

std::mutex + std::condition_variable

Базовые примитивы синхронизации: mutex — взаимное исключение доступа к общим данным, condition_variable — ожидание, пока некоторое условие не станет истинным, без busy-wait.The basic synchronization primitives: mutex provides mutual exclusion for access to shared data, condition_variable provides waiting until some condition becomes true, without busy-waiting.

часто используетсяcommonly used wait — всегда с предикатомwait — always with a predicate частый вопрос: напиши bounded queue / thread poolcommon interview ask: write a bounded queue / thread pool

Что этоWhat it is

std::mutex — примитив взаимного исключения: lock()/unlock() гарантируют, что только один поток одновременно находится в критической секции. std::condition_variable — примитив ожидания: поток блокируется на wait(), пока другой поток не вызовет notify_one()/notify_all(), освобождая mutex на время сна и захватывая его обратно перед возвратом из wait.std::mutex is a mutual-exclusion primitive: lock()/unlock() guarantee that only one thread is in the critical section at a time. std::condition_variable is a waiting primitive: a thread blocks on wait() until another thread calls notify_one()/notify_all(), releasing the mutex while asleep and reacquiring it before returning from wait.

до C++11 — pthread напрямуюbefore C++11 — raw pthread
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t  cv = PTHREAD_COND_INITIALIZER;
pthread_mutex_lock(&m);
while (!ready) pthread_cond_wait(&cv, &m);
pthread_mutex_unlock(&m);
std::mutex + std::condition_variable
std::mutex m;
std::condition_variable cv;
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [] { return ready; });   // предикат защищает// the predicate guards
                                       // от spurious wakeup// against spurious wakeup

Зачем ввелиWhy it was introduced

Как и std::thread, эти примитивы закрывают разрыв между потребностью и отсутствием стандартного решения:Like std::thread, these primitives close the gap between an existing need and the lack of a standard solution:

01

RAII-управление блокировкой вместо ручного lock/unlockRAII lock management instead of manual lock/unlock

pthread_mutex_unlock, забытый на пути исключения, оставляет mutex захваченным навсегда — std::lock_guard/std::unique_lock освобождают mutex в деструкторе гарантированно, независимо от пути выхода из функции.A pthread_mutex_unlock forgotten along an exception path leaves the mutex locked forever — std::lock_guard/std::unique_lock release the mutex in their destructor unconditionally, regardless of how the function exits.

02

Портируемость между платформамиPortability across platforms

Та же причина, что и у std::thread — pthread_mutex/pthread_cond не существуют на Windows в этом виде, единый стандартный интерфейс убирает платформенно-зависимый код синхронизации.The same reason as with std::thread — pthread_mutex/pthread_cond don't exist in this form on Windows; a single standard interface removes platform-specific synchronization code.

03

Формальная привязка к memory modelA formal tie-in to the memory model

std::mutex формально специфицирован в терминах happens-before из новой memory model C++11 — освобождение mutex synchronizes-with последующий его захват другим потоком, что даёт строгую гарантию видимости данных, записанных под локом.std::mutex is formally specified in terms of the happens-before relation from the new C++11 memory model — releasing a mutex synchronizes-with a subsequent acquire of it by another thread, giving a strict guarantee about the visibility of data written under the lock.

Как это работает под капотомHow it works under the hood

На Linux std::mutex обычно реализован через futex (fast userspace mutex) — быстрый путь без syscall при отсутствии contention, с падением в syscall (блокировка потока планировщиком) только когда мьютекс уже занят. Полный разбор паттерна producer/consumer через mutex + condition_variable (включая обязательность предиката в wait против spurious wakeups) — в cpp_concurrency.md, здесь — происхождение и минимальный пример.On Linux, std::mutex is typically implemented via a futex (fast userspace mutex) — a fast path with no syscall when there's no contention, falling back to a syscall (the scheduler blocking the thread) only when the mutex is already held. A full breakdown of the producer/consumer pattern using mutex + condition_variable (including why a predicate in wait is mandatory against spurious wakeups) lives in cpp_concurrency.md — here it's just the origin and a minimal example.

#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex m;
std::condition_variable cv;
std::queue<int> q;

void producer() {
    {
        std::lock_guard<std::mutex> lk(m);
        q.push(42);
    }                      // lock_guard освобождает mutex здесь// lock_guard releases the mutex here
    cv.notify_one();       // будить можно и вне лока// notifying outside the lock is fine too
}

void consumer() {
    std::unique_lock<std::mutex> lk(m);   // unique_lock, не lock_guard —// unique_lock, not lock_guard —
                                            // нужен для wait()// needed for wait()
    cv.wait(lk, [] { return !q.empty(); }); // предикат обязателен// the predicate is mandatory
    int v = q.front(); q.pop();
}

Своя реализацияRolling your own

bounded queue — практическое задание поверх mutex + condvarbounded queue — a practical exercise built on mutex + condvar

Не «реализация фичи», а типовое практическое задание, которое собирается именно из mutex + condition_variable: потокобезопасная очередь с ограниченной ёмкостью, блокирующая producer при заполнении и consumer при опустошении.Not a "feature implementation" but a typical practical exercise built exactly out of mutex + condition_variable: a thread-safe queue with bounded capacity that blocks the producer when full and the consumer when empty.

bounded_queue.hpp

template <class T>
class BoundedQueue {
    std::mutex m_;
    std::condition_variable not_full_, not_empty_;
    std::deque<T> q_;
    size_t cap_;
public:
    explicit BoundedQueue(size_t cap) : cap_(cap) {}

    void push(T v) {
        std::unique_lock<std::mutex> lk(m_);
        not_full_.wait(lk, [&] { return q_.size() < cap_; }); // предикат обязателен// the predicate is mandatory
        q_.push_back(std::move(v));
        lk.unlock();           // можно уведомить вне лока// notifying outside the lock is fine
        not_empty_.notify_one();
    }

    T pop() {
        std::unique_lock<std::mutex> lk(m_);
        not_empty_.wait(lk, [&] { return !q_.empty(); });
        T v = std::move(q_.front());
        q_.pop_front();
        lk.unlock();
        not_full_.notify_one();
        return v;
    }
};

Логичное развитие того же задания — thread pool: N воркеров в цикле вызывают pop() на общей BoundedQueue задач (обычно std::function<void()>), а push() используется для постановки задачи в очередь; завершение пула — специальный «poison pill» объект или отдельный atomic-флаг остановки, который воркеры проверяют в предикате wait. Полный разбор паттерна — в cpp_concurrency.md.The logical next step from the same exercise is a thread pool: N workers loop calling pop() on a shared BoundedQueue of tasks (usually std::function<void()>), while push() enqueues work; shutting the pool down uses a special "poison pill" object or a separate atomic stop flag that workers check in the wait predicate. A full breakdown of the pattern lives in cpp_concurrency.md.

ГраблиGotchas

wait() без предиката — spurious wakeupwait() without a predicate — spurious wakeup

cv.wait(lk) без предиката может вернуться из ожидания без соответствующего notify (spurious wakeup — допустимо по стандарту на некоторых платформах) — код, полагающийся на «раз проснулись — значит условие истинно», содержит баг. Версия с предикатом (cv.wait(lk, pred)) эквивалентна циклу while (!pred()) wait(lk) и safe by construction.cv.wait(lk) without a predicate can return from waiting without a matching notify (a spurious wakeup — permitted by the standard on some platforms) — code that assumes "we woke up, so the condition must be true" has a bug. The predicate version (cv.wait(lk, pred)) is equivalent to a while (!pred()) wait(lk) loop and is safe by construction.

lock_guard нельзя использовать с condition_variable::waitlock_guard can't be used with condition_variable::wait

wait() требует unique_lock, потому что ему нужно временно разблокировать mutex на время ожидания и снова заблокировать перед возвратом — у lock_guard нет методов lock()/unlock(), он проще и дешевле, но не подходит для этого случая.wait() requires a unique_lock because it needs to temporarily unlock the mutex while waiting and relock it before returning — lock_guard has no lock()/unlock() methods; it's simpler and cheaper, but doesn't fit this case.

Двойной lock одного и того же std::mutex в одном потоке — deadlockDouble-locking the same std::mutex on one thread — deadlock

std::mutex не рекурсивен: повторный lock() из того же потока, который уже держит мьютекс, — undefined behavior (на практике часто deadlock). Для намеренно рекурсивных случаев есть отдельный тип std::recursive_mutex, но его использование обычно сигнализирует о проблемах в дизайне кода.std::mutex isn't recursive: a repeated lock() from the same thread that already holds the mutex is undefined behavior (in practice, often a deadlock). For intentionally recursive cases there's a separate std::recursive_mutex type, but needing it usually signals a design problem.