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_cstM, стоящее перед 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_fenceX, такая что A упорядочена перед X, а X стоит перед B в едином полном порядке; - или существует операция
memory_order_seq_cstatomic_thread_fenceY, такая что Y упорядочена перед B, а A стоит перед Y в едином полном порядке; - или существуют операции
memory_order_seq_cstatomic_thread_fenceX и Y, такие что A упорядочена перед X, Y упорядочена перед B, а X стоит перед Y в едином полном порядке.
Обратите внимание, что это означает следующее:
memory_order_seq_cst, последовательная согласованность теряется;Последовательное упорядочивание может понадобиться в ситуациях с несколькими производителями и несколькими потребителями, когда все потребители должны наблюдать действия всех производителей в одинаковом порядке.
Для полного последовательного порядка на всех многопроцессорных системах требуется инструкция процессора для полного барьера памяти. Это может стать узким местом производительности, поскольку заставляет затронутые обращения к памяти распространяться на все ядра.
Связь с 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