Namespaces
Variants

memory_order

From cppreference.net
Определено в заголовочном файле <stdatomic.h>
enum memory_order

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

} ;
(начиная с 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 , плюс существует единый общий порядок, в котором все потоки наблюдают все модификации в одном и том же порядке (см. Последовательно-согласованное упорядочивание ниже).

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

Атомарные операции с тегом 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_cst atomic_thread_fence X такой, что A упорядочен-перед X и X появляется перед B в Едином Общем Порядке,
  • или существует memory_order_seq_cst atomic_thread_fence Y такой, что Y упорядочен-перед B и A появляется перед Y в Едином Общем Порядке,
  • или существуют memory_order_seq_cst atomic_thread_fence X и Y такие, что A упорядочен-перед X, Y упорядочен-перед B, и X появляется перед Y в Едином Общем Порядке.

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

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

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

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

Связь с 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