std::memory_order
Определён в заголовке <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) | Операция загрузки с этим порядком памяти выполняет операцию consume для затронутой ячейки памяти: чтения и записи в текущем потоке, зависящие от загруженного значения, не могут быть переупорядочены перед этой загрузкой. Записи в переменные, зависящие от данных, в других потоках, освобождающих ту же атомарную переменную, видимы в текущем потоке. На большинстве платформ это влияет только на оптимизации компилятора (см. раздел Порядок release-consume ниже). |
memory_order_acquire | Операция загрузки с этим порядком памяти выполняет операцию acquire для затронутой ячейки памяти: чтения и записи в текущем потоке не могут быть переупорядочены перед этой загрузкой. Все записи в других потоках, освобождающих ту же атомарную переменную, видимы в текущем потоке (см. раздел Порядок release-acquire ниже). |
memory_order_release | Операция сохранения с этим порядком памяти выполняет операцию release: чтения и записи в текущем потоке не могут быть переупорядочены после этой операции сохранения. Все записи в текущем потоке видимы в других потоках, приобретающих ту же атомарную переменную (см. раздел Порядок release-acquire ниже), а записи, несущие зависимость в атомарную переменную, становятся видимыми в других потоках, потребляющих ту же атомарную переменную (см. раздел Порядок release-consume ниже). |
memory_order_acq_rel | Операция чтения-модификации-записи с этим порядком памяти является одновременно операцией acquire и операцией release. Чтения и записи памяти в текущем потоке не могут быть переупорядочены ни перед загрузкой, ни после сохранения. Все записи в других потоках, освобождающих ту же атомарную переменную, видимы до модификации, а сама модификация видима в других потоках, приобретающих ту же атомарную переменную. |
memory_order_seq_cst | Операция загрузки с этим порядком памяти выполняет операцию acquire, операция сохранения выполняет операцию release, а операция чтения-модификации-записи выполняет и операцию acquire, и операцию release. Кроме того, существует единый полный порядок, в котором все потоки наблюдают все модификации в одном и том же порядке (см. раздел Последовательно согласованный порядок ниже). |
Формальное описание
Межпоточная синхронизация и порядок памяти определяют, как вычисления и побочные эффекты выражений упорядочены между разными потоками выполнения. Они определяются следующими терминами:
Упорядочено перед
Внутри одного потока вычисление 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. | (до C++26) |
Порядок модификации
Все модификации конкретной атомарной переменной происходят в едином порядке, относящемся только к этой атомарной переменной.
Для всех атомарных операций гарантируются следующие четыре требования:
Последовательность release
После выполнения операции release A над атомарным объектом M самая длинная непрерывная подпоследовательность порядка модификации M, состоящая из:
| 1) Записей, выполненных тем же потоком, что и A. | (до C++20) |
Называется последовательностью release с началом в A.
Синхронизируется с
Если атомарное сохранение в потоке A является операцией release, атомарная загрузка из той же переменной в потоке B является операцией acquire, а загрузка в потоке B читает значение, записанное сохранением в потоке A, то сохранение в потоке A синхронизируется с загрузкой в потоке B.
Кроме того, некоторые вызовы библиотеки могут быть определены как синхронизирующиеся с другими вызовами библиотеки в других потоках.
Упорядочено по зависимости передМежду потоками вычисление A упорядочено по зависимости перед вычислением B, если выполняется одно из следующих условий: 1) A выполняет операцию release над некоторой атомарной переменной M, а в другом потоке B выполняет операцию consume над той же атомарной переменной M, и B читает значение, записанное любой частью последовательности release с началом(до C++20) в A. 2) A упорядочено по зависимости перед X, а X несёт зависимость в B. | (до C++26) |
Межпоточно происходит до
Между потоками вычисление A межпоточно происходит до вычисления B, если выполняется одно из следующих условий:
Происходит доНезависимо от потоков вычисление A происходит до вычисления B, если выполняется одно из следующих условий: 1) A упорядочено перед B. 2) A межпоточно происходит до B.Реализация должна обеспечить ацикличность отношения происходит до, при необходимости добавив синхронизацию (это может потребоваться только при участии операции consume; см. Batty и др.). Если одно вычисление изменяет ячейку памяти, а другое читает или изменяет ту же ячейку, и хотя бы одно из вычислений не является атомарной операцией, поведение программы не определено (в программе возникает состояние гонки данных), если между этими двумя вычислениями не существует отношения происходит до.
| (до C++26) | ||
Происходит доНезависимо от потоков вычисление A происходит до вычисления B, если выполняется одно из следующих условий: 1) A упорядочено перед B. 2) A синхронизируется с B. 3) A происходит до X, а X происходит до B. | (начиная с C++26) |
Строго происходит до
Независимо от потоков вычисление 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 просто(до C++26) происходит до Y, а Y упорядочено перед B. 4) A строго происходит до X, а X строго происходит до B. Примечание: неформально, если A строго происходит до B, то во всех контекстах кажется, что A вычисляется перед B.
| (начиная с C++20) |
Видимые побочные эффекты
Побочный эффект A над скалярной переменной M (запись) является видимым относительно вычисления значения B для M (чтения), если выполняются оба следующих условия:
Если побочный эффект A видим относительно вычисления значения B, то самая длинная непрерывная подпоследовательность побочных эффектов для M в порядке модификации, относительно которой B не происходит до, называется видимой последовательностью побочных эффектов (значение M, определённое B, будет значением, сохранённым одним из этих побочных эффектов).
Стандарт требует, чтобы реализации обеспечивали видимость операций атомарного сохранения для других операций атомарной загрузки за разумное время (но не устанавливает требования к тому, что считать «разумным временем»).
Примечание: межпоточная синхронизация сводится к предотвращению состояний гонки данных (посредством установления отношений «происходит до») и определению того, какие побочные эффекты становятся видимыми и при каких условиях. Видимость данных тесно связана с кэшированием процессора.
Операция consume
Атомарная загрузка с memory_order_consume или более строгим порядком является операцией consume. Обратите внимание, что std::atomic_thread_fence предъявляет более строгие требования к синхронизации, чем операция consume.
Операция acquire
Атомарная загрузка с memory_order_acquire или более строгим порядком является операцией acquire. Операция lock() над мьютексом также является операцией acquire. Обратите внимание, что std::atomic_thread_fence предъявляет более строгие требования к синхронизации, чем операция acquire.
Операция release
Атомарное сохранение с memory_order_release или более строгим порядком является операцией release. Операция unlock() над мьютексом также является операцией 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&&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 — из-за переупорядочивания компилятором или во время выполнения.
| Даже при ослабленной модели памяти значения, возникшие из ниоткуда, не могут циклически зависеть от собственных вычислений. Например, если // 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&&r2==42, поскольку сохранение | (начиная с 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.
Все записи в память (включая неатомарные записи и атомарные записи с ослабленным порядком), которые с точки зрения потока A произошли до атомарного сохранения, становятся видимыми побочными эффектами в потоке B. Иными словами, после завершения атомарной загрузки поток B гарантированно увидит всё, что поток A записал в память. Это гарантируется только в том случае, если B действительно получает значение, сохранённое A, либо значение из последующей части последовательности release.
Синхронизация устанавливается только между потоками, которые освобождают и приобретают одну и ту же атомарную переменную. Другие потоки могут наблюдать порядок обращений к памяти, отличный от порядка, наблюдаемого одним или обоими синхронизированными потоками.
В системах со строгим порядком — x86, SPARC TSO, мейнфреймах IBM и т. д. — порядок release-acquire автоматически обеспечивается для большинства операций. Дополнительные инструкции процессора для этого режима синхронизации не генерируются; затрагиваются только некоторые оптимизации компилятора (например, компилятору запрещается перемещать неатомарные записи за атомарную операцию store-release или выполнять неатомарные загрузки раньше атомарной операции load-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 помечено как Все записи в память (неатомарные и атомарные с ослабленным порядком), которые с точки зрения потока A произошли до атомарного сохранения, становятся видимыми побочными эффектами для тех операций в потоке B, в которые операция загрузки несёт зависимость. Иными словами, после завершения атомарной загрузки те операторы и функции потока B, которые используют полученное при загрузке значение, гарантированно увидят то, что поток A записал в память. Синхронизация устанавливается только между потоками, которые освобождают и потребляют одну и ту же атомарную переменную. Другие потоки могут наблюдать порядок обращений к памяти, отличный от порядка, наблюдаемого одним или обоими синхронизированными потоками. На всех распространённых процессорах, кроме DEC Alpha, упорядочение зависимостей обеспечивается автоматически; для этого режима синхронизации дополнительные инструкции процессора не генерируются, затрагиваются только некоторые оптимизации компилятора (например, компилятору запрещается выполнять спекулятивные загрузки объектов, участвующих в цепочке зависимостей). Типичные случаи применения этого порядка включают чтение редко изменяемых конкурентных структур данных (таблиц маршрутизации, конфигурации, политик безопасности, правил брандмауэра и т. д.), а также сценарии издатель-подписчик с публикацией через указатель. В таких сценариях производитель публикует указатель, через который потребитель может получить доступ к информации: нет необходимости делать видимым для потребителя всё остальное, что производитель записал в память (это может быть дорогостоящей операцией на архитектурах со слабым порядком). Пример такого сценария — Для точного управления цепочкой зависимостей см. также Обратите внимание, что в настоящее время (02.2015) неизвестны промышленные компиляторы, отслеживающие цепочки зависимостей: операции consume преобразуются в операции acquire. | (до C++26) |
| Спецификация порядка release-consume пересматривается, поэтому использование |
(начиная с C++17) (до C++26) |
| Порядок release-consume имеет тот же эффект, что и порядок release-acquire, и является устаревшим. | (начиная с 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 (всё, что произошло до сохранения в одном потоке, становится видимым побочным эффектом в потоке, выполнившем загрузку), но и устанавливают единый полный порядок модификации всех атомарных операций с такой меткой.
| Формально, каждая операция
Если перед B в порядке упорядочено перед находится операция
Для пары атомарных операций над M, обозначенных A и B, где A записывает значение, а B читает его, если существуют две операции
Для пары атомарных модификаций M, обозначенных A и B, B происходит после A в порядке модификации M, если
Обратите внимание, что это означает следующее: 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 и 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. Единый полный порядок может быть не согласован с отношением происходит до. Это позволяет реализовать Например, если // 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 может дать результат Обратите внимание: 1) как только появляются атомарные операции без меткиmemory_order_seq_cst, гарантия последовательной согласованности программы теряется, 2) во многих случаях атомарные операции memory_order_seq_cst могут переупорядочиваться относительно других атомарных операций, выполняемых тем же потоком. | (начиная с 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 не являются атомарными (одновременное чтение и запись представляют собой гонку данных) и не упорядочивают память (доступы к памяти, не помеченной volatile, могут свободно перемещаться относительно доступа к volatile).
Примечательным исключением является Visual Studio: при настройках по умолчанию каждая запись volatile имеет семантику release, а каждое чтение volatile — семантику acquire (документация Microsoft), поэтому volatile можно использовать для межпоточной синхронизации. Стандартная семантика volatile неприменима к многопоточному программированию, хотя её достаточно, например, для взаимодействия с обработчиком std::signal, работающим в том же потоке, если она используется с переменными типа sig_atomic_t. Чтобы восстановить поведение, соответствующее стандарту, можно использовать параметр компилятора /volatile:iso. По умолчанию он включён, если целевой платформой является ARM.
См. также
| Документация по C о порядке памяти |
Внешние ссылки
| 1. | Протокол MOESI |
| 2. | x86-TSO: строгая и практичная модель программирования для многопроцессорных систем x86 P. Sewell и др., 2010 |
| 3. | Вводное руководство по моделям ослабленной памяти ARM и POWER P. Sewell и др., 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/cpp/atomic/memory_order