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

std::atomic + memory model

Первая формальная модель памяти в стандарте 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.

data race = undefined behavior happens-before

Что этоWhat it is

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.

до C++11 — понятия «поток» нет в стандартеbefore C++11 — no notion of a "thread" in the standard
// 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 — формальная гарантияstd::atomic — a formal guarantee
std::atomic<bool> ready{false};
// store/load с заданным memory_order —// store/load with an explicit memory_order —
// формально специфицированное поведение// formally specified behavior
// относительно других потоков// with respect to other threads

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

Формальная модель памяти была необходима не как удобство, а как условие корректности любого многопоточного кода:A formal memory model wasn't a nicety — it was a precondition for the correctness of any multithreaded code:

01

До C++11 компилятор не обязан был учитывать потокиBefore C++11, the compiler had no obligation to account for threads

Стандарт языка не признавал существование потоков исполнения — компилятор формально мог переупорядочивать или устранять обращения к памяти так, как было бы некорректно при реальном параллельном исполнении, потому что с точки зрения стандарта «параллельного исполнения не существовало».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."

02

volatile не решает задачу синхронизацииvolatile doesn't solve the synchronization problem

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.

03

Нужен способ синхронизации дешевле mutexA synchronization mechanism cheaper than a mutex was needed

Для простых случаев — счётчики, флаги готовности, индексы в 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.

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

Подробный разбор 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."

ГраблиGotchas

volatile — не замена std::atomicvolatile is not a substitute for std::atomic

Частая путаница у программистов, пришедших из 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 — undefined behavior, не «прочитал старое значение»A data race is undefined behavior, not "read a stale value"

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

seq_cst — безопасный дефолт, но не бесплатныйseq_cst is a safe default, but not free

Операции без явного 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.