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 | Операция чтения с данным порядком памяти выполняет операцию потребления над затронутой ячейкой памяти: никакие чтения или записи в текущем потоке, зависящие от загруженного значения, не могут быть переупорядочены до этого чтения. Записи в данные-зависимые переменные в других потоках, которые освобождают ту же атомную переменную, становятся видимыми в текущем потоке. На большинстве платформ это влияет только на оптимизации компилятора (см. Упорядочение типа Выпуск-Потребление ниже). |
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 == 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.
Обратите внимание, что в настоящее время (февраль 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 mainframe и т. д. — упорядочение типа выпуск-захват является автоматическим для большинства операций. Для этой модели синхронизации не выдаются дополнительные инструкции процессора, влияют только определённые оптимизации компилятора (например, компилятор запрещается перемещать неатомарные записи после атомарной записи-освобождения или выполнять неатомарные загрузки раньше атомарной загрузки-захвата). На системах со слабым упорядочением (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.
Если была 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_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 lvalues не могут быть переупорядочены мимо наблюдаемых побочных эффектов (включая другие обращения volatile), разделенные точкой последовательности внутри того же потока, но этот порядок не гарантируется для наблюдения другим потоком, поскольку volatile доступ не устанавливает межпоточную синхронизацию.
Кроме того, обращения volatile не являются атомарными (конкурентное чтение и запись — это гонка данных) и не упорядочивают память (не-volatile обращения к памяти могут быть свободно переупорядочены вокруг обращения volatile).
Одно заметное исключение — Visual Studio, где при стандартных настройках каждая volatile запись имеет семантику release, а каждая volatile чтение имеет семантику acquire (Microsoft Docs), и поэтому volatile может использоваться для межпоточной синхронизации. Стандартные volatile семантики не применимы к многопоточному программированию, хотя они достаточны, например, для связи с signal обработчиком, работающим в том же потоке, когда применяются к sig_atomic_t переменным.
Примеры
Ссылки
- Стандарт 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/w/c/atomic/memory_order