Spec-Zone.ru › C

memory_order

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

Ослабленный порядок

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

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

// Thread 1: r1 = atomic_load_explicit(y, memory_order_relaxed); // A atomic_store_explicit(x, r1, memory_order_relaxed); // B // Thread 2: r2 = atomic_load_explicit(x, memory_order_relaxed); // C atomic_store_explicit(y, 42, 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 — из-за переупорядочивания компилятором или во время выполнения.

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

Порядок освобождения и потребления

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

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

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

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

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

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

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

Если атомарная переменная сохранена с семантикой освобождения, а несколько других потоков выполняют над этой переменной операции чтения-изменения-записи, формируется «последовательность освобождения»: все потоки, выполняющие операции чтения-изменения-записи над одной и той же атомарной переменной, синхронизируются с первым потоком и друг с другом, даже если их операции не имеют семантики memory_order_release. Это позволяет реализовать ситуации «один производитель — несколько потребителей», не вводя ненужную синхронизацию между отдельными потоками-потребителями.

Порядок освобождения и получения

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

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

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

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

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

Последовательно согласованный порядок

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

Формально:

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

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

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

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

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

Связь с volatile

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

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

Примечательное исключение — Visual Studio: при настройках по умолчанию каждая запись volatile имеет семантику освобождения, а каждое чтение volatile — семантику получения (документация Microsoft), поэтому 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++ для порядка памяти

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

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/c/atomic/memory_order

Spec-Zone.ru

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