memory_order
|
Определено в заголовочном файле
<stdatomic.h>
|
||
|
enum
memory_order
{
|
(начиная с C11) | |
memory_order
определяет, каким образом обращения к памяти, включая обычные неатомарные обращения, должны упорядочиваться вокруг атомарной операции. При отсутствии ограничений в многопроцессорной системе, когда несколько потоков одновременно читают и записывают в различные переменные, один поток может наблюдать изменение значений в порядке, отличном от порядка, в котором другой поток их записывал. Более того, видимый порядок изменений может различаться даже между несколькими потоками-читателями. Некоторые схожие эффекты могут возникать даже в однопроцессорных системах из-за преобразований компилятора, разрешенных моделью памяти.
Поведение по умолчанию всех атомарных операций в
языке
и библиотеке обеспечивает
последовательно согласованное упорядочение
(см. обсуждение ниже). Это поведение по умолчанию может ухудшить производительность, но атомарным операциям библиотеки может быть передан дополнительный аргумент
memory_order
для указания точных ограничений (помимо атомарности), которые компилятор и процессор должны соблюдать для данной операции.
Содержание |
Константы
|
Определено в заголовочном файле
<stdatomic.h>
|
|
| Значение | Объяснение |
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 , плюс существует единый общий порядок, в котором все потоки наблюдают все модификации в одном и том же порядке (см. Последовательно-согласованное упорядочивание ниже). |
|
Этот раздел не завершен
Причина: happens-before и другие концепции, как в C++, но сохранить порядки модификаций и четыре консистентности в c/language/atomic |
|
Этот раздел не завершён
Причина: при выполнении вышеуказанного не забывайте, что хотя отношение happens-before не было ациклическим в C11 в опубликованной версии, это было исправлено для соответствия C++11 через DR 401 |
Ослабленное упорядочение
Атомарные операции с тегом memory_order_relaxed не являются операциями синхронизации; они не накладывают порядок на конкурентные обращения к памяти. Они гарантируют только атомарность и согласованность порядка модификаций.
Например, при x и y изначально равных нулю,
// Поток 1:
r1
=
atomic_load_explicit
(
y, memory_order_relaxed
)
;
// A
atomic_store_explicit
(
x, r1, memory_order_relaxed
)
;
// B
// Поток 2:
r2
=
atomic_load_explicit
(
x, memory_order_relaxed
)
;
// C
atomic_store_explicit
(
y,
42
, 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 — либо из-за переупорядочения компилятором, либо во время выполнения.
Типичное использование ослабленного порядка памяти — это инкрементирование счетчиков, таких как reference counters, поскольку это требует только атомарности, но не порядка или синхронизации.
Упорядочение Release-Consume
Если атомарная запись в потоке A помечена тегом memory_order_release , атомарное чтение в потоке B из той же переменной помечено тегом memory_order_consume , и чтение в потоке B получает значение, записанное операцией записи в потоке A, тогда запись в потоке A упорядочена по зависимостям перед чтением в потоке B.
Все записи в память (неатомарные и релаксированные атомарные), которые happened-before атомарной записи с точки зрения потока A, становятся visible side-effects в тех операциях потока B, в которые операция загрузки carries dependency , то есть, как только атомарная загрузка завершена, те операторы и функции в потоке B, которые используют значение, полученное из загрузки, гарантированно видят то, что поток A записал в память.
Синхронизация устанавливается только между потоками, которые освобождают и потребляют одну и ту же атомарную переменную. Другие потоки могут видеть иной порядок операций доступа к памяти, чем один или оба синхронизированных потока.
На всех распространённых процессорах, кроме DEC Alpha, упорядочивание зависимостей происходит автоматически, для этого режима синхронизации не требуется дополнительных инструкций процессора, затрагиваются только определённые оптимизации компилятора (например, компилятору запрещено выполнять спекулятивные загрузки объектов, участвующих в цепочке зависимостей).
Типичные сценарии использования этого порядка доступа включают чтение редко изменяемых конкурентных структур данных (таблицы маршрутизации, конфигурации, политики безопасности, правила брандмауэра и т.д.) и ситуации издатель-подписчик с публикацией через указатели, то есть когда производитель публикует указатель, через который потребитель может получить доступ к информации: нет необходимости делать видимыми для потребителя все остальные данные, записанные производителем в память (что может быть дорогой операцией на слабо упорядоченных архитектурах). Примером такого сценария является
rcu_dereference
.
Обратите внимание, что на данный момент (2/2015) ни один известный производственный компилятор не отслеживает цепочки зависимостей: операции consume преобразуются в операции acquire.
Последовательность освобождения
Если некоторый атомарный объект сохраняется с операцией store-release и несколько других потоков выполняют операции чтения-изменения-записи над этим атомарным объектом, формируется "релизная последовательность": все потоки, выполняющие чтение-изменение-запись того же атомарного объекта, синхронизируются с первым потоком и друг с другом, даже если они не имеют
memory_order_release
семантики. Это делает возможными ситуации с единственным производителем и множеством потребителей без наложения избыточной синхронизации между отдельными потоками-потребителями.
Упорядочение 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, или значение из последующей части release sequence.
Синхронизация устанавливается только между потоками, которые освобождают и захватывают одну и ту же атомарную переменную. Другие потоки могут видеть иной порядок операций доступа к памяти, чем один или оба синхронизированных потока.
На системах с сильной упорядоченностью — x86, SPARC TSO, мейнфреймы IBM и т.д. — упорядочение release-acquire является автоматическим для большинства операций. Для этого режима синхронизации не генерируются дополнительные инструкции процессора; затрагиваются только определенные оптимизации компилятора (например, компилятору запрещено перемещать неатомарные записи после атомарной store-release или выполнять неатомарные чтения раньше атомарной load-acquire). На системах со слабой упорядоченностью (ARM, Itanium, PowerPC) используются специальные инструкции процессора для загрузки или барьеров памяти.
Взаимные блокировки исключения, такие как мьютексы или атомарные спинлоки , являются примером синхронизации по принципу отпускания-захвата: когда блокировка освобождается потоком A и захватывается потоком B, все, что происходило в критической секции (до освобождения) в контексте потока A, должно стать видимым для потока B (после захвата), который выполняет ту же критическую секцию.
Последовательно-согласованное упорядочение
Атомарные операции с тегом memory_order_seq_cst не только упорядочивают память так же, как упорядочение release/acquire (все, что happened-before сохранения в одном потоке, становится видимым побочным эффектом в потоке, выполнившем загрузку), но также устанавливают единый общий порядок модификаций всех атомарных операций, помеченных этим тегом.
Формально,
каждая
memory_order_seq_cst
операция B, которая загружает из атомарной переменной 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
atomic_thread_fence
X,
упорядоченная-перед
B, тогда B наблюдает одно из следующего:
-
последнее
memory_order_seq_cstизменение M, которое появляется перед X в едином общем порядке, - некоторое несвязанное изменение M, которое появляется позже в порядке изменений M.
Для пары атомарных операций над M, называемых A и B, где A записывает, а B читает значение M, если существуют два
memory_order_seq_cst
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_cstatomic_thread_fence X такой, что A упорядочен-перед X и X появляется перед B в Едином Общем Порядке, -
или существует
memory_order_seq_cstatomic_thread_fence Y такой, что Y упорядочен-перед B и A появляется перед Y в Едином Общем Порядке, -
или существуют
memory_order_seq_cstatomic_thread_fence X и Y такие, что A упорядочен-перед X, Y упорядочен-перед B, и X появляется перед Y в Едином Общем Порядке.
Обратите внимание, что это означает:
memory_order_seq_cst
, последовательная согласованность теряется,
Последовательное упорядочивание может быть необходимо в ситуациях с несколькими производителями и несколькими потребителями, где все потребители должны наблюдать действия всех производителей, происходящие в одном и том же порядке.
Полное последовательное упорядочивание требует инструкции полного барьера памяти на всех многопроцессорных системах. Это может стать узким местом производительности, поскольку принудительно распространяет затрагиваемые обращения к памяти на каждое ядро.
Связь с volatile
В потоке выполнения обращения (чтения и записи) через volatile lvalues не могут быть переупорядочены относительно наблюдаемых побочных эффектов (включая другие volatile-обращения), разделенных точкой следования в том же потоке, однако этот порядок не гарантируется для наблюдения из другого потока, поскольку volatile-доступ не устанавливает межпоточную синхронизацию.
Кроме того, доступ к volatile-объектам не является атомарным (параллельное чтение и запись представляет собой data race ) и не упорядочивает память (не-volatile доступы к памяти могут свободно переупорядочиваться относительно volatile доступа).
Одним заметным исключением является Visual Studio, где при настройках по умолчанию каждая запись в volatile имеет семантику освобождения, а каждое чтение volatile — семантику захвата (
Microsoft Docs
), поэтому volatile может использоваться для межпоточной синхронизации. Стандартные
volatile
семантики не применимы к многопоточному программированию, хотя они достаточны для, например, взаимодействия с
signal
обработчиком, который выполняется в том же потоке, когда применяется к
sig_atomic_t
переменным. Опция компилятора
/volatile:iso
может использоваться для восстановления поведения, соответствующего стандарту, что является настройкой по умолчанию при целевой платформе ARM.
Примеры
|
Этот раздел не завершён
Причина: отсутствует пример |
Ссылки
- Стандарт C23 (ISO/IEC 9899:2024):
-
- 7.17.1/4 memory_order (стр.: TBD)
-
- 7.17.3 Порядок и согласованность (стр.: TBD)
- Стандарт C17 (ISO/IEC 9899:2018):
-
- 7.17.1/4 memory_order (стр: 200)
-
- 7.17.3 Порядок и согласованность (стр: 201-203)
- Стандарт C11 (ISO/IEC 9899:2011):
-
- 7.17.1/4 memory_order (стр: 273)
-
- 7.17.3 Порядок и согласованность (стр: 275-277)
Смотрите также
|
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 |
|
Этот раздел является неполным
Причина: Нужно найти хорошие ссылки по QPI, MOESI и, возможно, Dragon. |