Spec-Zone.ru › C++

std::memory_order

Определено в заголовочном файле <atomic>
typedef enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
} memory_order;
(с 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 Операция загрузки с данным порядком памяти выполняет операцию потребления в указанном месте памяти: никакие чтения или записи в текущем потоке, зависящие от загруженного значения, не могут быть переупорядочены перед этой загрузкой. Записи в переменные, зависящие от данных, в других потоках, которые освобождают ту же атомарную переменную, видны в текущем потоке. На большинстве платформ это влияет только на оптимизации компилятора (см. Упорядочение выпуска-потребления ниже).
memory_order_acquire Операция загрузки с этим порядком памяти выполняет операцию захвата в указанном месте памяти: никакие чтения или записи в текущем потоке не могут быть переупорядочены перед этой загрузкой. Все записи в других потоках, которые освобождают ту же атомарную переменную, видны в текущем потоке (см. Упорядочение выпуска-приобретения ниже).
memory_order_release Операция сохранения с этим порядком памяти выполняет операцию выпуска: никакие чтения или записи в текущем потоке не могут быть переупорядочены после этого сохранения. Все записи в текущем потоке видны в других потоках, которые приобретают ту же атомарную переменную (см. Упорядочение выпуска-приобретения ниже) и записи, которые вносят зависимость в атомарную переменную, становятся видимыми в других потоках, которые потребляют ту же атомарную переменную (см. Упорядочение выпуска-потребления ниже).
memory_order_acq_rel Операция чтения-модификации-записи с этим порядком памяти является как операцией захвата, так и операцией выпуска. Никакие чтения или записи памяти в текущем потоке не могут быть переупорядочены перед загрузкой, ни после сохранения. Все записи в других потоках, которые освобождают ту же атомарную переменную, видны перед модификацией, и модификация видна в других потоках, которые приобретают ту же атомарную переменную.
memory_order_seq_cst Операция загрузки с этим порядком памяти выполняет операцию захвата, сохранение выполняет операцию выпуска, а чтение-модификация-запись выполняет как операцию захвата, так и операцию выпуска, плюс существует единственный общий порядок, в котором все потоки наблюдают все модификации в одном порядке (см. Последовательно согласованное упорядочение ниже).

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

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

Последовательность вычислений

Внутри одного потока вычисление A может быть последовательно-до вычисления B, как описано в порядке вычисления.

Несет зависимость

Внутри одного потока вычисление 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.

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

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

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

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 предшествует вычислению значения (чтения) B объекта M, то вычисление B должно получить свое значение из X или из побочного эффекта Y, который следует за X в порядке модификаций M.

Последовательность выпуска

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

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

Известна как последовательность выпуска, возглавляемая A.

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

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

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

Зависимость упорядочена до

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

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

Межпотоковое предшествует

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

1) A синхронизируется с B.
2) A зависимость упорядочена до B.
3) A синхронизируется с некоторым вычислением X, и X последовательно упорядочено до B.
4) A последовательно упорядочено до некоторого вычисления X, и X предшествует в других потоках B.
5) A предшествует в других потоках некоторому вычислению X, и X предшествует в других потоках B.

Предшествует

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

1) A последовательно упорядочено до B.
2) A предшествует в других потоках B.

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

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

Просто предшествует

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

1) A последовательно упорядочено до B. 2) A синхронизируется с B. 3) A просто предшествует X, и X просто предшествует B.

Примечание: без операций потребления отношения просто предшествует и предшествует одинаковы.

(с C++20)

Сильно предшествует

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

1) A предшествует B. 2) A синхронизируется с B. 3) A сильно предшествует X, и X сильно предшествует B. (до C++20)
1) A предшествует B. 2) A синхронизируется с B, и оба A и B — атомарные операции последовательной согласованности. 3) A предшествует X, X просто предшествует Y, и Y предшествует B. 4) A сильно предшествует X, и X сильно предшествует B.

Примечание: неформально, если A сильно предшествует B, то A, по-видимому, оценивается перед B во всех контекстах.

Примечание: сильное предшествование исключает операции consume.

(с C++20)

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

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

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

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

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

Операция consume

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

Операция acquire

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

Операция release

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

Объяснение

Расслабленная упорядоченность

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

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

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

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

Даже при ослабленной модели памяти запрещено появление значений из ниоткуда, зависящих от собственных вычислений, например, при 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, что циклически зависит от записи 42 в y. Обратите внимание, что до C++14 это технически допускалось спецификацией, но не рекомендовалось разработчикам.

(с C++14)

Типичное применение для расслабленной упорядоченности памяти — инкрементирование счетчиков, таких как счётчики ссылок std::shared_ptr, так как это требует только атомарности, но не упорядочения или синхронизации (обратите внимание, что декрементирование счетчиков 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.

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

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

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

Взаимные исключающие блокировки, такие как std::mutex или атомарный спинлок, являются примером синхронизации release-acquire: когда блокировка освобождается потоком 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"); // never fires
    assert(data == 42); // never fires
}
 
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

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

#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 записал в память.

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

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

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

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

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

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

(с C++17)

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

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

Формально,

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

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

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

  • последнее memory_order_seq_cst изменение M, которое предшествует X в единственном полном порядке,
  • некоторое несвязанное изменение M, которое появляется позже в порядке изменений M.

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

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

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

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

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

1) как только атомарные операции, которые не помечены memory_order_seq_cst, появляются в картине, последовательная согласованность теряется, 2) последовательно согласованные барьеры устанавливают только общий порядок для самих барьеров, а не для атомарных операций в общем случае (предшествует — это не межпотоковая связь, в отличие от предшествует).
(до 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_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 могут быть переупорядочены относительно других атомарных операций, выполненных тем же потоком.
(с C++20)

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

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

Этот пример демонстрирует ситуацию, где необходим последовательный порядок. Любой другой порядок может вызвать утверждение, потому что потоки 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), которые предшествуют или следуют в том же потоке, но этот порядок не гарантируется для наблюдения другим потоком, так как доступ к volatile не устанавливает межпотоковую синхронизацию.

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

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

См. также

C документация для порядка памяти

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

1. Протокол MOESI
2. x86-TSO: Строгий и удобный для программиста модель памяти для x86 многопроцессорных систем P. Sewell et. al., 2010
3. Вводный урок по релаксированным моделям памяти ARM и POWER P. Sewell et al, 2012
4. MESIF: Двухэтапный протокол согласованности кэша для соединений точка-точка J.R. Goodman, H.H.J. Hum, 2009
5. Модели памяти Russ Cox, 2021

© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/w/cpp/atomic/memory_order

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API