многопоточностьconcurrency
Базовые примитивы синхронизации: 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.
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.
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 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
Как и std::thread, эти примитивы закрывают разрыв между потребностью и отсутствием стандартного решения:Like std::thread, these primitives close the gap between an existing need and the lack of a standard solution:
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.
Та же причина, что и у 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.
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.
На 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();
}
Не «реализация фичи», а типовое практическое задание, которое собирается именно из 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.
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.
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.
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.