многопоточностьconcurrency
Первая формальная модель памяти в стандарте C++ и типы с атомарными операциями без блокировок. До C++11 понятие «поток» вообще не существовало на уровне языка — оптимизации компилятора могли ломать многопоточный код непредсказуемо.The first formal memory model in the C++ standard, plus types with lock-free atomic operations. Before C++11, the notion of a "thread" didn't exist at the language level at all — compiler optimizations could break multithreaded code unpredictably.
std::atomic<T> — шаблонный тип из <atomic>, операции над которым (load, store, fetch_add, compare_exchange и т.д.) гарантированно неделимы (atomic) с точки зрения других потоков — нет промежуточного состояния, которое можно было бы наблюдать. Вместе с типом стандарт вводит формальную модель памяти: строгое определение data race как undefined behavior и отношение happens-before, определяющее, когда запись одного потока гарантированно видна чтению другого.std::atomic<T> is a template type from <atomic> whose operations (load, store, fetch_add, compare_exchange, etc.) are guaranteed indivisible (atomic) from the point of view of other threads — there's no intermediate state that could be observed. Alongside the type, the standard introduces a formal memory model: a strict definition of data races as undefined behavior, and the happens-before relation, which determines when a write on one thread is guaranteed visible to a read on another.
// volatile int flag; — НЕ синхронизация, только запрет// volatile int flag; — NOT synchronization, only suppresses
// оптимизации компилятора на конкретной переменной;// compiler optimizations on that specific variable;
// не даёт memory ordering между потоками// gives no memory ordering between threads
std::atomic<bool> ready{false};
// store/load с заданным memory_order —// store/load with an explicit memory_order —
// формально специфицированное поведение// formally specified behavior
// относительно других потоков// with respect to other threads
Формальная модель памяти была необходима не как удобство, а как условие корректности любого многопоточного кода:A formal memory model wasn't a nicety — it was a precondition for the correctness of any multithreaded code:
Стандарт языка не признавал существование потоков исполнения — компилятор формально мог переупорядочивать или устранять обращения к памяти так, как было бы некорректно при реальном параллельном исполнении, потому что с точки зрения стандарта «параллельного исполнения не существовало».The language standard didn't acknowledge the existence of threads of execution — the compiler was formally free to reorder or eliminate memory accesses in ways that would be incorrect under real concurrent execution, because as far as the standard was concerned "concurrent execution didn't exist."
volatile (частая до-C++11 путаница) предотвращает оптимизации компилятора над конкретным доступом (нужно для MMIO/сигналов), но не даёт никаких гарантий порядка операций между разными переменными и разными потоками — использование volatile для межпоточной синхронизации было и остаётся ошибкой.volatile (a common pre-C++11 point of confusion) prevents compiler optimizations on a specific access (needed for MMIO/signal handlers), but gives no guarantees about ordering between different variables and different threads — using volatile for cross-thread synchronization was, and still is, a bug.
Для простых случаев — счётчики, флаги готовности, индексы в lock-free структурах — полноценный mutex избыточно дорог; атомарные операции с явно управляемым memory_order дают точную гарантию порядка без блокировки потока.For simple cases — counters, readiness flags, indices in lock-free structures — a full mutex is needlessly expensive; atomic operations with an explicitly controlled memory_order give a precise ordering guarantee without blocking the thread.
Подробный разбор memory_order (relaxed/acquire/release/seq_cst), паттерна публикации данных через atomic-флаг, false sharing и SPSC lock-free очередей — в cpp_concurrency.md. Здесь — происхождение фичи и базовый пример: атомарная запись данных с release, атомарное чтение флага с acquire формируют happens-before, которая гарантирует видимость записанных данных.A detailed breakdown of memory_order (relaxed/acquire/release/seq_cst), the data-publication pattern via an atomic flag, false sharing, and SPSC lock-free queues lives in cpp_concurrency.md. Here it's just the feature's origin and a basic example: an atomic store of the data with release, and an atomic load of the flag with acquire, together form a happens-before relation that guarantees visibility of the written data.
#include <atomic>
std::atomic<bool> ready{false};
int data = 0;
// producer:// producer:
void produce() {
data = 42; // обычная запись// an ordinary write
ready.store(true, std::memory_order_release); // публикация// publication
}
// consumer:// consumer:
void consume() {
while (!ready.load(std::memory_order_acquire)) {} // ждём публикации// wait for publication
use(data); // гарантированно видит data == 42 — release/acquire// guaranteed to see data == 42 — release/acquire
// формируют happens-before между потоками// form a happens-before relation between threads
}
Без пары release/acquire (например, если использовать memory_order_relaxed для обеих операций) чтение data в consumer было бы data race — undefined behavior, а не просто «может прочитать старое значение».Without the release/acquire pair (for example, using memory_order_relaxed for both operations), reading data in the consumer would be a data race — undefined behavior, not merely "might read a stale value."
Частая путаница у программистов, пришедших из C или embedded — volatile не даёт атомарности операций (инкремент volatile-переменной — не атомарен) и не создаёт happens-before между потоками. Единственное законное применение volatile — доступ к memory-mapped I/O и обработчики сигналов, не межпоточная синхронизация.A common confusion among programmers coming from C or embedded work — volatile doesn't make operations atomic (incrementing a volatile variable is not atomic) and doesn't create a happens-before relation between threads. The one legitimate use of volatile is memory-mapped I/O and signal handlers, not cross-thread synchronization.
Если минимум один поток пишет в переменную без синхронизации, пока другой поток её читает или пишет, — это data race, и стандарт объявляет поведение всей программы неопределённым, а не гарантирует «просто увидеть немного устаревшее значение». Компилятор формально может делать с таким кодом что угодно.If at least one thread writes to a variable without synchronization while another thread reads or writes it, that's a data race, and the standard declares the behavior of the entire program undefined — it does not guarantee "you'll just see a slightly stale value." The compiler is formally free to do anything with such code.
Операции без явного memory_order используют memory_order_seq_cst по умолчанию — самый строгий и самый предсказуемый режим, но и самый дорогой на некоторых архитектурах. Ослаблять до relaxed/acquire/release стоит только когда профайлер показал contention именно здесь, не заранее.Operations with no explicit memory_order default to memory_order_seq_cst — the strictest and most predictable mode, but also the most expensive on some architectures. Relaxing to relaxed/acquire/release is worth doing only once a profiler has shown contention right here, not preemptively.