Namespaces
Variants

std:: memory_order

From cppreference.net
Concurrency support library
Threads
(C++11)
(C++20)
this_thread namespace
(C++11)
(C++11)
Cooperative cancellation
Mutual exclusion
Generic lock management
Condition variables
(C++11)
Semaphores
Latches and Barriers
(C++20)
(C++20)
Futures
(C++11)
(C++11)
(C++11)
Safe reclamation
Hazard pointers
Atomic types
(C++11)
(C++20)
Initialization of atomic types
(C++11) (deprecated in C++20)
(C++11) (deprecated in C++20)
Memory ordering
memory_order
(C++11)
(C++11) (deprecated in C++26)
Free functions for atomic operations
Free functions for atomic flags
Определено в заголовочном файле <atomic>
enum memory_order

{
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst

} ;
(начиная с C++11)
(до C++20)
enum class memory_order : /* unspecified */

{
relaxed, consume, acquire, release, acq_rel, seq_cst
} ;
inline constexpr memory_order memory_order_relaxed = memory_order :: relaxed ;
inline constexpr memory_order memory_order_consume = memory_order :: consume ;
inline constexpr memory_order memory_order_acquire = memory_order :: acquire ;
inline constexpr memory_order memory_order_release = memory_order :: release ;
inline constexpr memory_order memory_order_acq_rel = memory_order :: acq_rel ;

inline constexpr memory_order memory_order_seq_cst = memory_order :: seq_cst ;
(начиная с C++20)

std::memory_order определяет, каким образом операции с памятью, включая обычные неатомарные обращения к памяти, должны упорядочиваться относительно атомарной операции. При отсутствии ограничений в многопроцессорной системе, когда несколько потоков одновременно читают и записывают несколько переменных, один поток может наблюдать изменение значений в порядке, отличном от порядка, в котором другой поток их записывал. Более того, видимый порядок изменений может различаться даже между несколькими потоками-читателями. Некоторые схожие эффекты могут возникать даже в однопроцессорных системах из-за преобразований компилятора, разрешённых моделью памяти.

Поведение по умолчанию всех атомарных операций в библиотеке обеспечивает последовательно согласованное упорядочение (см. обсуждение ниже). Это поведение по умолчанию может снижать производительность, но атомарным операциям библиотеки может быть передан дополнительный аргумент std::memory_order для указания точных ограничений (помимо атомарности), которые компилятор и процессор должны соблюдать для данной операции.

Содержание

Константы

Определено в заголовочном файле <atomic>
Название Значение
memory_order_relaxed Расслабленная операция: не накладывает ограничений синхронизации или упорядочивания на другие операции чтения или записи, гарантируется только атомарность данной операции (см. Расслабленное упорядочивание ниже).
memory_order_consume
(устарело в C++26)
Операция загрузки с этим порядком памяти выполняет операцию потребления для затронутой области памяти: никакие операции чтения или записи в текущем потоке, зависящие от загруженного значения, не могут быть переупорядочены перед этой загрузкой. Записи в зависимые переменные в других потоках, которые освобождают ту же атомарную переменную, видны в текущем потоке. На большинстве платформ это влияет только на оптимизации компилятора (см. Упорядочивание "освобождение-потребление" ниже).
memory_order_acquire Операция загрузки с этим порядком памяти выполняет операцию захвата для затронутой области памяти: никакие операции чтения или записи в текущем потоке не могут быть переупорядочены перед этой загрузкой. Все записи в других потоках, которые освобождают ту же атомарную переменную, видны в текущем потоке (см. Упорядочивание "освобождение-захват" ниже).
memory_order_release Операция сохранения с этим порядком памяти выполняет операцию освобождения : никакие операции чтения или записи в текущем потоке не могут быть переупорядочены после этой операции сохранения. Все записи в текущем потоке видны в других потоках, которые захватывают ту же атомарную переменную (см. Упорядочивание "освобождение-захват" ниже), и записи, которые несут зависимость в атомарную переменную, становятся видны в других потоках, которые потребляют ту же атомарную переменную (см. Упорядочивание "освобождение-потребление" ниже).
memory_order_acq_rel Операция чтения-изменения-записи с этим порядком памяти является одновременно и операцией захвата , и операцией освобождения . Никакие операции чтения или записи памяти в текущем потоке не могут быть переупорядочены перед загрузкой или после сохранения. Все записи в других потоках, которые освобождают ту же атомарную переменную, видны до модификации, и модификация видна в других потоках, которые захватывают ту же атомарную переменную.
memory_order_seq_cst Операция загрузки с этим порядком памяти выполняет операцию захвата , операция сохранения выполняет операцию освобождения , а операция чтения-изменения-записи выполняет и операцию захвата , и операцию освобождения , плюс существует единый общий порядок, в котором все потоки наблюдают все модификации в одном и том же порядке (см. Последовательно-согласованное упорядочивание ниже).

Формальное описание

Межпоточная синхронизация и упорядочение памяти определяют, как вычисления и побочные эффекты выражений упорядочиваются между различными потоками выполнения. Они определяются следующими терминами:

Упорядочено-перед

В рамках одного потока вычисление A может быть sequenced-before вычислением B, как описано в разделе evaluation order .

Перенос зависимости

В пределах одного потока вычисление A, которое упорядочено-перед вычислением B, также может переносить зависимость в B (то есть B зависит от A), если выполняется любое из следующих условий:

1) Значение A используется в качестве операнда B, за исключением
a) если B является вызовом std::kill_dependency ,
b) если A является левым операндом встроенных операторов && , || , ?: , или , .
2) A записывает значение в скалярный объект M, B читает из M.
3) A переносит зависимость в другое вычисление X, и X переносит зависимость в B.
(до C++26)

Порядок модификации

Все изменения любого конкретного атомарного переменного происходят в полном порядке, который специфичен именно для этой одной атомарной переменной.

Следующие четыре требования гарантируются для всех атомарных операций:

1) Когерентность записи-записи : Если вычисление A, модифицирующее атомарную переменную M (запись) происходит-до вычисления B, модифицирующего M, тогда A появляется раньше B в порядке модификаций M.
2) Когерентность чтения-чтения : если вычисление значения A некоторого атомарного объекта M (чтение) происходит-до вычисления значения B на M, и если значение A получено из записи X на M, тогда значение B является либо значением, сохранённым X, либо значением, сохранённым побочным эффектом Y на M, который появляется позже X в порядке модификации M.
3) Когерентность чтения-записи : если вычисление значения A некоторого атомарного объекта M (чтение) происходит-до операции B над M (запись), тогда значение A происходит из побочного эффекта (записи) X, который появляется раньше B в порядке модификации M.
4) Когерентность записи-чтения : если побочный эффект (запись) X на атомарном объекте M happens-before вычисление значения (чтение) B объекта M, тогда вычисление B должно брать своё значение из X или из побочного эффекта Y, который следует за X в порядке модификации M.

Последовательность освобождения

После выполнения операции освобождения A над атомарным объектом M, самая длинная непрерывная подпоследовательность порядка модификации M, состоящая из:

1) Записи, выполненные тем же потоком, который выполнил A.
(until C++20)
2) Атомарные операции чтения-модификации-записи, выполняемые любым потоком в отношении M.

Известно как release sequence headed by A .

Синхронизируется с

Если атомарная запись в потоке A является операцией освобождения (release operation) , атомарное чтение в потоке B из той же переменной является операцией захвата (acquire operation) , и чтение в потоке B получает значение, записанное операцией записи в потоке A, тогда запись в потоке A синхронизируется-с (synchronizes-with) чтением в потоке B.

Кроме того, некоторые вызовы библиотечных функций могут быть определены для синхронизации-с другими вызовами библиотечных функций в других потоках.

Упорядочено по зависимости перед

Между потоками, вычисление A упорядочено по зависимости перед вычислением B, если выполняется любое из следующих условий:

1) A выполняет release операцию на некотором атомарном M, и, в другом потоке, B выполняет consume операцию на том же атомарном M, и B читает значение, записанное любой частью релиз последовательности, возглавляемой (до C++20) A.
2) A упорядочено по зависимости перед X и X переносит зависимость в B.
(до C++26)

Межпоточное отношение happens-before

Между потоками, вычисление A межпоточно предшествует вычислению B, если выполняется любое из следующих условий:

1) A synchronizes-with B.
2) A является dependency-ordered before B.
3) A synchronizes-with некоторое вычисление X, и X sequenced-before B.
4) A является sequenced-before для некоторого вычисления X, и X inter-thread happens-before B.
5) A inter-thread happens-before некоторое вычисление X, и X inter-thread happens-before B.


Happens-before

Независимо от потоков, вычисление A happens-before вычисление B, если выполняется любое из следующих условий:

1) A является sequenced-before B.
2) A inter-thread happens before B.

Реализация обязана обеспечивать ацикличность отношения happens-before , вводя дополнительную синхронизацию при необходимости (это может потребоваться только при участии операции consume, см. Batty et al ).

Если одно вычисление изменяет область памяти, а другое читает или изменяет ту же область памяти, и если хотя бы одно из вычислений не является атомарной операцией, поведение программы не определено (программа имеет data race ), если между этими двумя вычислениями не существует отношения happens-before .

Simply happens-before

Независимо от потоков, вычисление A simply happens-before вычисление B, если выполняется любое из следующих условий:

1) A является sequenced-before B.
2) A synchronizes-with B.
3) A simply happens-before X, и X simply happens-before B.

Примечание: без операций consume отношения simply happens-before и happens-before совпадают.

(since C++20)
(until C++26)

Happens-before

Независимо от потоков, вычисление A happens-before вычисление B, если выполняется любое из следующих условий:

1) A является sequenced-before B.
2) A synchronizes-with B.
3) A happens-before X, и X happens-before B.
(since C++26)

Сильно предшествует (Strongly happens-before)

Независимо от потоков, вычисление A строго предшествует вычислению B, если выполняется любое из следующих условий:

1) A sequenced-before B.
2) A synchronizes-with B.
3) A strongly happens-before X, и X strongly happens-before B.
(до C++20)
1) A sequenced-before B.
2) A synchronizes with B, и оба A и B являются последовательно согласованными атомарными операциями.
3) A sequenced-before X, X simply (до C++26) happens-before Y, и Y sequenced-before B.
4) A strongly happens-before X, и X strongly happens-before B.

Примечание: неформально, если A strongly happens-before B, то A выглядит так, как будто вычисляется до B во всех контекстах.

Примечание: strongly happens-before исключает операции consume.

(до C++26)
(начиная с C++20)

Видимые побочные эффекты

Побочный эффект A над скаляром M (запись) является видимым по отношению к вычислению значения B над M (чтение), если выполняются оба следующих условия:

1) A happens-before B.
2) Не существует другого побочного эффекта X для M, где A happens-before X и X happens-before B.

Если побочный эффект A виден относительно вычисления значения B, то наибольшее непрерывное подмножество побочных эффектов в M, в порядке модификации , где B не происходит-до него, известно как видимая последовательность побочных эффектов (значение M, определяемое B, будет значением, сохранённым одним из этих побочных эффектов).

Примечание: синхронизация между потоками сводится к предотвращению состояний гонки (путем установления отношений happens-before) и определению того, какие побочные эффекты становятся видимыми при каких условиях.

Операция потребления

Атомарная загрузка с memory_order_consume или более строгим порядком является операцией потребления. Заметьте, что std::atomic_thread_fence накладывает более строгие требования синхронизации, чем операция потребления.

Операция Acquire

Атомарная загрузка с memory_order_acquire или более строгим порядком является операцией захвата. Операция lock() на Mutex также является операцией захвата. Обратите внимание, что std::atomic_thread_fence накладывает более строгие требования синхронизации, чем операция захвата.

Операция освобождения

Атомарная запись с memory_order_release или более строгим порядком является операцией освобождения. Операция unlock() на Mutex также является операцией освобождения. Обратите внимание, что std::atomic_thread_fence накладывает более строгие требования синхронизации, чем операция освобождения.

Объяснение

Ослабленное упорядочение

Атомарные операции с тегом memory_order_relaxed не являются операциями синхронизации; они не накладывают порядок на параллельные обращения к памяти. Они гарантируют только атомарность и согласованность порядка модификаций.

Например, при x и y изначально равных нулю,

// Поток 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Поток 2:
r2 = x.load(std::memory_order_relaxed); // C 
y.store(42, std::memory_order_relaxed); // D

разрешено получать r1 == r2 == 42 потому что, хотя A sequenced-before B в потоке 1 и C sequenced before D в потоке 2, ничто не мешает D появиться перед A в порядке модификации y , а B появиться перед C в порядке модификации x . Побочный эффект D на y может быть видим для загрузки A в потоке 1, в то время как побочный эффект B на x может быть видим для загрузки C в потоке 2. В частности, это может произойти, если D завершается до C в потоке 2, либо из-за переупорядочивания компилятором, либо во время выполнения.

Даже при ослабленной модели памяти, значения "из ниоткуда" (out-of-thin-air) не могут циклически зависеть от собственных вычислений, например, при x и y изначально равных нулю,

// Thread 1:
r1 = y.load(std::memory_order_relaxed);
if (r1 == 42)
    x.store(r1, std::memory_order_relaxed);
// Thread 2:
r2 = x.load(std::memory_order_relaxed);
if (r2 == 42)
    y.store(42, std::memory_order_relaxed);

не допускается получение r1 == r2 == 42 , поскольку сохранение 42 в y возможно только если сохранение в x сохраняет 42 , что циклически зависит от сохранения в y значения 42 . Отметим, что до C++14 это технически допускалось спецификацией, но не рекомендовалось для реализаторов.

(since C++14)

Типичное использование ослабленного порядка памяти — инкрементирование счетчиков, таких как счетчики ссылок std::shared_ptr , поскольку это требует только атомарности, но не упорядочивания или синхронизации (обратите внимание, что декрементирование std::shared_ptr счетчиков требует синхронизации acquire-release с деструктором).

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
std::atomic<int> cnt = {0};
void f()
{
    for (int n = 0; n < 1000; ++n)
        cnt.fetch_add(1, std::memory_order_relaxed);
}
int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n)
        v.emplace_back(f);
    for (auto& t : v)
        t.join();
    std::cout << "Final counter value is " << cnt << '\n';
}

Вывод:

Final counter value is 10000

Упорядочение Release-Acquire

Если атомарная запись в потоке A помечена тегом memory_order_release , атомарное чтение в потоке B из той же переменной помечено тегом memory_order_acquire , и чтение в потоке B получает значение, записанное операцией записи в потоке A, тогда запись в потоке A синхронизируется-с чтением в потоке B.

Все записи в память (включая неатомарные и релаксированные атомарные), которые happened-before атомарной записи с точки зрения потока A, становятся visible side-effects в потоке B. То есть, как только атомарное чтение завершено, поток B гарантированно видит всё, что поток A записал в память. Это обещание действует только если B фактически возвращает значение, которое сохранил A, или значение из более поздней части последовательности выпуска.

Синхронизация устанавливается только между потоками, которые освобождают и захватывают одну и ту же атомарную переменную. Другие потоки могут видеть иной порядок операций доступа к памяти, чем один или оба синхронизированных потока.

На системах с сильной упорядоченностью — x86, SPARC TSO, мейнфреймы IBM и т.д. — упорядочение release-acquire является автоматическим для большинства операций. Для этого режима синхронизации не генерируются дополнительные инструкции процессора; затрагиваются только определенные оптимизации компилятора (например, компилятору запрещено перемещать неатомарные записи после атомарной store-release или выполнять неатомарные чтения раньше атомарной load-acquire). На системах со слабой упорядоченностью (ARM, Itanium, PowerPC) используются специальные инструкции процессора для загрузки или барьеров памяти.

Мьютексы, такие как std::mutex или атомарный спинлок , являются примером синхронизации по схеме освобождение-захват: когда блокировка освобождается потоком A и захватывается потоком B, все, что происходило в критической секции (до освобождения) в контексте потока A, должно быть видно потоку B (после захвата), который выполняет ту же критическую секцию.

#include <atomic>
#include <cassert>
#include <string>
#include <thread>
std::atomic<std::string*> ptr;
int data;
void producer()
{
    std::string* p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_acquire)))
        ;
    assert(*p2 == "Hello"); // никогда не срабатывает
    assert(data == 42); // никогда не срабатывает
}
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

Следующий пример демонстрирует транзитивное упорядочение release-acquire между тремя потоками с использованием release sequence.

#include <atomic>
#include <cassert>
#include <thread>
#include <vector>
std::vector<int> data;
std::atomic<int> flag = {0};
void thread_1()
{
    data.push_back(42);
    flag.store(1, std::memory_order_release);
}
void thread_2()
{
    int expected = 1;
    // memory_order_relaxed is okay because this is an RMW,
    // and RMWs (with any ordering) following a release form a release sequence
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed))
    {
        expected = 1;
    }
}
void thread_3()
{
    while (flag.load(std::memory_order_acquire) < 2)
        ;
    // if we read the value 2 from the atomic flag, we see 42 in the vector
    assert(data.at(0) == 42); // will never fire
}
int main()
{
    std::thread a(thread_1);
    std::thread b(thread_2);
    std::thread c(thread_3);
    a.join(); b.join(); c.join();
}

Упорядочение Release-Consume

Если атомарная запись в потоке A помечена тегом memory_order_release , атомарное чтение в потоке B из той же переменной помечено тегом memory_order_consume , и чтение в потоке B получает значение, записанное операцией записи в потоке A, тогда запись в потоке A является упорядоченной по зависимостям перед чтением в потоке B.

Все операции записи в память (неатомарные и релаксированные атомарные), которые произошли-до атомарной записи с точки зрения потока A, становятся видимыми побочными эффектами в тех операциях потока B, в которые операция чтения переносит зависимость , то есть, как только атомарное чтение завершено, гарантируется, что те операторы и функции в потоке B, которые используют значение, полученное из операции чтения, увидят то, что поток A записал в память.

Синхронизация устанавливается только между потоками, освобождающими и потребляющими одну и ту же атомарную переменную. Другие потоки могут видеть иной порядок обращений к памяти, чем один или оба синхронизированных потока.

На всех основных CPU, кроме DEC Alpha, упорядочивание по зависимостям происходит автоматически, для этого режима синхронизации не выдаются дополнительные инструкции CPU, затрагиваются только определенные оптимизации компилятора (например, компилятору запрещено выполнять спекулятивные загрузки объектов, участвующих в цепочке зависимостей).

Типичные случаи использования этого упорядочивания включают чтение редко изменяемых конкурентных структур данных (таблицы маршрутизации, конфигурации, политики безопасности, правила брандмауэра и т.д.) и ситуации издатель-подписчик с публикацией через указатель, то есть когда производитель публикует указатель, через который потребитель может получить доступ к информации: нет необходимости делать видимыми для потребителя все остальные записи в память, выполненные производителем (что может быть дорогой операцией на слабо упорядоченных архитектурах). Примером такого сценария является rcu_dereference .

Смотрите также std::kill_dependency и [[ carries_dependency ]] для детального управления цепочкой зависимостей.

Обратите внимание, что в настоящее время (2/2015) ни один известный промышленный компилятор не отслеживает цепочки зависимостей: операции consume повышаются до операций acquire.

(до C++26)

Спецификация упорядочения release-consume пересматривается, и использование memory_order_consume временно не рекомендуется.

(since C++17)
(until C++26)

Упорядочение release-consume имеет тот же эффект, что и упорядочение release-acquire, и является устаревшим.

(since C++26)

Этот пример демонстрирует синхронизацию с упорядочением по зависимостям для публикации через указатель: целочисленные данные не связаны с указателем на строку отношением зависимости по данным, поэтому их значение в потребителе не определено.

#include <atomic>
#include <cassert>
#include <string>
#include <thread>
std::atomic<std::string*> ptr;
int data;
void producer()
{
    std::string* p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_consume)))
        ;
    assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptr
    assert(data == 42); // may or may not fire: data does not carry dependency from ptr
}
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}


Последовательно-согласованное упорядочение

Атомарные операции с тегом memory_order_seq_cst не только упорядочивают память так же, как упорядочение release/acquire (все, что happened-before сохранения в одном потоке, становится видимым побочным эффектом в потоке, выполнившем загрузку), но также устанавливают единый общий порядок модификаций всех атомарных операций, помеченных этим тегом.

Формально,

каждая операция B с memory_order_seq_cst , которая читает из атомарной переменной M, наблюдает одно из следующего:

  • результат последней операции A, которая модифицировала M и появляется перед B в едином общем порядке,
  • ИЛИ, если такая A существовала, B может наблюдать результат некоторой модификации M, которая не является memory_order_seq_cst и не happens-before A,
  • ИЛИ, если такой A не было, B может наблюдать результат некоторой несвязанной модификации M, которая не является memory_order_seq_cst .

Если существовала операция memory_order_seq_cst std::atomic_thread_fence X, sequenced-before B, тогда B наблюдает одно из следующего:

  • последнюю модификацию M с memory_order_seq_cst , которая появляется перед X в едином общем порядке,
  • некоторую несвязанную модификацию M, которая появляется позже в порядке модификации M.

Для пары атомарных операций над M, называемых A и B, где A записывает, а B читает значение M, если существуют два memory_order_seq_cst std::atomic_thread_fence X и Y, и если A sequenced-before X, Y sequenced-before B, и X появляется перед Y в Едином Общем Порядке, тогда B наблюдает либо:

  • эффект A,
  • некоторую несвязанную модификацию M, которая появляется после A в порядке модификации M.

Для пары атомарных модификаций M, называемых A и B, B происходит после A в порядке модификации M, если:

  • существует memory_order_seq_cst std::atomic_thread_fence X такой, что A sequenced-before X и X появляется перед B в Едином Общем Порядке,
  • или, существует memory_order_seq_cst std::atomic_thread_fence Y такой, что Y sequenced-before B и A появляется перед Y в Едином Общем Порядке,
  • или, существуют memory_order_seq_cst std::atomic_thread_fence X и Y такие, что A sequenced-before X, Y sequenced-before B, и X появляется перед Y в Едином Общем Порядке.

Отметим, что это означает:

1) как только в игру вступают атомарные операции без тега memory_order_seq_cst , последовательная согласованность теряется,
2) последовательно-согласованные барьеры устанавливают общий порядок только для самих барьеров, а не для атомарных операций в общем случае ( sequenced-before не является межпоточным отношением, в отличие от happens-before ).
(until C++20)
Формально,

атомарная операция A над некоторым атомарным объектом M является упорядоченной-по-когерентности-перед другой атомарной операцией B над M, если выполняется любое из следующих условий:

1) A является модификацией, и B читает значение, сохранённое A,
2) A предшествует B в порядке модификаций M,
3) A читает значение, сохранённое атомарной модификацией X, X предшествует B в порядке модификаций , и A и B не являются одной и той же атомарной операцией чтения-модификации-записи,
4) A является упорядоченной-по-когерентности-перед X, и X является упорядоченной-по-когерентности-перед B.

Существует единый полный порядок S для всех операций memory_order_seq_cst , включая барьеры, который удовлетворяет следующим ограничениям:

1) если A и B являются операциями memory_order_seq_cst , и A сильно-происходит-перед B, то A предшествует B в S,
2) для каждой пары атомарных операций A и B над объектом M, где A упорядочена-по-когерентности-перед B:
a) если A и B обе являются операциями memory_order_seq_cst , то A предшествует B в S,
b) если A является операцией memory_order_seq_cst , и B происходит-перед барьером memory_order_seq_cst Y, то A предшествует Y в S,
c) если барьер memory_order_seq_cst X происходит-перед A, и B является операцией memory_order_seq_cst , то X предшествует B в S,
d) если барьер memory_order_seq_cst X происходит-перед A, и B происходит-перед барьером memory_order_seq_cst Y, то X предшествует Y в S.

Формальное определение гарантирует, что:

1) единый полный порядок согласован с порядком модификаций любого атомарного объекта,
2) загрузка memory_order_seq_cst получает своё значение либо из последней модификации memory_order_seq_cst , либо из некоторой модификации не- memory_order_seq_cst , которая не происходит-перед предшествующими модификациями memory_order_seq_cst .

Единый полный порядок может быть не согласован с происходит-перед . Это позволяет более эффективную реализацию memory_order_acquire и memory_order_release на некоторых процессорах. Это может приводить к неожиданным результатам, когда memory_order_acquire и memory_order_release смешиваются с memory_order_seq_cst .

Например, при x и y изначально равных нулю,

// Thread 1:
x.store(1, std::memory_order_seq_cst); // A
y.store(1, std::memory_order_release); // B
// Thread 2:
r1 = y.fetch_add(1, std::memory_order_seq_cst); // C
r2 = y.load(std::memory_order_relaxed); // D
// Thread 3:
y.store(3, std::memory_order_seq_cst); // E
r3 = x.load(std::memory_order_seq_cst); // F

может дать результат r1 == 1 && r2 == 3 && r3 == 0 , где A происходит-перед C, но C предшествует A в едином полном порядке C-E-F-A операций memory_order_seq_cst (см. Lahav et al ).

Важно отметить:

1) как только в программе появляются атомарные операции, не помеченные memory_order_seq_cst , гарантия последовательной согласованности для программы теряется,
2) во многих случаях атомарные операции memory_order_seq_cst могут быть переупорядочены относительно других атомарных операций, выполняемых тем же потоком.
(since C++20)

Последовательное упорядочивание может быть необходимо в ситуациях с несколькими производителями и несколькими потребителями, где все потребители должны наблюдать действия всех производителей, происходящие в одном и том же порядке.

Полное последовательное упорядочивание требует инструкции полного барьера памяти на всех многопроцессорных системах. Это может стать узким местом производительности, поскольку принудительно распространяет затрагиваемые обращения к памяти на каждое ядро.

Этот пример демонстрирует ситуацию, когда необходимо последовательное упорядочение. Любой другой порядок может вызвать срабатывание assert, поскольку потоки c и d могут наблюдать изменения атомарных переменных x и y в противоположном порядке.

#include <atomic>
#include <cassert>
#include <thread>
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
void write_x()
{
    x.store(true, std::memory_order_seq_cst);
}
void write_y()
{
    y.store(true, std::memory_order_seq_cst);
}
void read_x_then_y()
{
    while (!x.load(std::memory_order_seq_cst))
        ;
    if (y.load(std::memory_order_seq_cst))
        ++z;
}
void read_y_then_x()
{
    while (!y.load(std::memory_order_seq_cst))
        ;
    if (x.load(std::memory_order_seq_cst))
        ++z;
}
int main()
{
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join(); b.join(); c.join(); d.join();
    assert(z.load() != 0); // will never happen
}

Связь с volatile

В потоке выполнения обращения (чтения и записи) через volatile glvalues не могут быть переупорядочены относительно наблюдаемых побочных эффектов (включая другие volatile-обращения), которые sequenced-before или sequenced-after в том же потоке, однако этот порядок не гарантирован для наблюдения из другого потока, поскольку volatile-доступ не устанавливает межпоточную синхронизацию.

Кроме того, обращения к volatile не являются атомарными (параллельное чтение и запись представляет собой data race ) и не упорядочивают память (не-volatile обращения к памяти могут свободно переупорядочиваться вокруг volatile обращения).

Одним заметным исключением является Visual Studio, где при настройках по умолчанию каждая запись в volatile имеет семантику освобождения, а каждое чтение volatile — семантику захвата ( Microsoft Docs ), поэтому volatile может использоваться для межпоточной синхронизации. Стандартная volatile семантика не применима к многопоточному программированию, хотя её достаточно для, например, взаимодействия с обработчиком std::signal , который выполняется в том же потоке, когда применяется к переменным типа sig_atomic_t . Опция компилятора /volatile:iso может использоваться для восстановления поведения, соответствующего стандарту, что является настройкой по умолчанию при целевой платформе ARM.

Смотрите также

Документация C для memory order

Внешние ссылки

1. MOESI protocol
2. x86-TSO: A Rigorous and Usable Programmer’s Model for x86 Multiprocessors P. Sewell и др., 2010
3. A Tutorial Introduction to the ARM and POWER Relaxed Memory Models P. Sewell и др., 2012
4. MESIF: A Two-Hop Cache Coherency Protocol for Point-to-Point Interconnects J.R. Goodman, H.H.J. Hum, 2009
5. Memory Models Russ Cox, 2021