Разница между memory_order_consume и memory_order_acquire

У меня есть вопрос относительно статьи GCC-Wiki. Под заголовком Общая сводка приведен следующий пример кода:

Тема 1:

y.store (20);
x.store (10);

Тема 2:

if (x.load() == 10) {
  assert (y.load() == 20)
  y.store (10)
}

Говорят, что если все хранилища являются освобождаемыми, а все загрузки получаем, утверждение в потоке 2 не может завершиться ошибкой. Мне это ясно (поскольку сохранение в x в потоке 1 синхронизируется с загрузкой из x в потоке 2).

Но теперь начинается часть, которую я не понимаю. Также говорится, что если все хранилища являются выпускными, а все загрузки являются потребляемыми, результаты будут одинаковыми. Разве не может быть, чтобы нагрузка от y поднималась до нагрузки от x (поскольку между этими переменными нет зависимости)? Это означало бы, что утверждение в потоке 2 на самом деле может завершиться ошибкой.


person splotz90    schedule 13.08.2015    source источник
comment
Эта часть комментария из статьи не относится к опубликованному вами коду.   -  person this    schedule 13.08.2015
comment
Почему нет? Комментарий к статье относится к коду, который я разместил. Я пропустил только код из темы 3 (потому что эта часть не имела отношения к этому вопросу).   -  person splotz90    schedule 13.08.2015
comment
В статье буквально говорится, что утверждение может завершиться ошибкой при использовании режима Release/Acquire: d(), утверждение потока 3 может завершиться ошибкой.   -  person this    schedule 13.08.2015
comment
@this Но поток 3 не имеет отношения к примеру OP. В статье также говорится, что Утверждение в потоке 2 все еще должно быть истинным, поскольку потоки 1 и 2 синхронизируются с x.load() при использовании release+acquire, а затем Если хранилища освобождены и нагрузки потребляются, результаты такие же, как при выпуске/получении, за исключением того, что может потребоваться меньше аппаратной синхронизации.   -  person dyp    schedule 14.08.2015
comment
@dyp Да. Я уточнил свой вопрос.   -  person splotz90    schedule 15.08.2015
comment
Жаль, что у нас есть два почти одинаково высоко оцененных ответа, которые противоречат друг другу.   -  person dyp    schedule 23.08.2015
comment
@dyp Это правда. Но если кто-то прочитает оба ответа (и все комментарии), все должно быть понятно. Оба ответа полезны, и я рад, что на этот вопрос ответили сейчас.   -  person splotz90    schedule 26.08.2015
comment
@ splotz90 Я не совсем понимаю вашу точку зрения: если ответ Iwillnotexist правильный, то (по крайней мере) половина ответа Йенса неверна. Кстати, я отправил автору этой статьи GCC электронное письмо, но либо я ошибся адресом (странная штука со СПАМФРИ), либо он еще не ответил.   -  person dyp    schedule 27.08.2015
comment
@dyp Я имел в виду, что пример кода Йенса полезен для понимания разницы между приобретением и потреблением. Но что касается этого конкретного вопроса, я также думаю, что объяснение не совсем правильное. Спасибо, что отправили письмо. Надеюсь, он получил письмо.   -  person splotz90    schedule 29.08.2015
comment
@dyp Теперь я еще больше уверен в правильности своего ответа, когда я проверил рассматриваемый код с помощью модели CppMem. Все в редакции моего ответа ниже.   -  person Iwillnotexist Idonotexist    schedule 02.09.2015
comment
@IwillnotexistIdonotexist Спасибо за доказательство. Я не знал об этом инструменте и не мог проверить его на своей машине (x86-64), потому что в этой архитектуре все загрузки выполняются автоматически, а все сохранения автоматически освобождаются.   -  person splotz90    schedule 02.09.2015
comment
@dyp Есть новости от автора статьи GCC?   -  person Iwillnotexist Idonotexist    schedule 08.09.2015
comment
@IwillnotexistIdonotexist К сожалению, нет. Возможно, вы захотите отправить электронное письмо самостоятельно, что снизит риск того, что я неправильно введу адрес и письмо застрянет в спам-фильтре.   -  person dyp    schedule 08.09.2015
comment
@dyp Я еще не получил ответа, но, просматривая список рассылки, я нашел это электронное письмо некоего Пола Э. Кенни с обсуждением похожего примера, и он считает, что утверждение не выдержит, ссылаясь на точно такие же архитектуры (ARM и PowerPC) в качестве ссылки в моем ответ ниже.   -  person Iwillnotexist Idonotexist    schedule 08.09.2015
comment
@IwillnotexistIdonotexist Вау, это длинная тема. Я быстро просмотрел связанное электронное письмо, но не смог найти вторую потребляемую загрузку.   -  person dyp    schedule 08.09.2015
comment
@dyp Фактически это оператор return b в первом блоке кода в кавычках. Сразу после указанного блока кода он говорит: Приведенный выше пример может иметь возвращаемое значение 0, если напрямую перевести его в ARM или Power, верно? Оба они могут преобразовывать чтение в условное выражение, и оба могут преобразовывать потребляющую нагрузку в простую загрузку, если зависимости данных остаются неразрывными.   -  person Iwillnotexist Idonotexist    schedule 09.09.2015
comment
@IwillnotexistIdonotexist Насколько я могу судить, return b должен загружать b неатомарно (поэтому предлагаемая альтернатива использования приобретения имеет смысл). Йенс Густедт утверждает, что существует гарантированный порядок между двумя атомарными потребительскими нагрузками.   -  person dyp    schedule 09.09.2015


Ответы (2)


Постановление стандарта C11 заключается в следующем.

5.1.2.4 Многопоточные выполнения и гонки данных

  1. Оценка A упорядочивается по зависимостям раньше 16) оценки B, если:

    A выполняет операцию выпуска для атомарного объекта M, а в другом потоке B выполняет операцию потребления для M и считывает значение, записанное любым побочным эффектом в последовательности выпуска, возглавляемой A, или

    - для некоторой оценки X, A упорядочен по зависимостям до того, как X и X перенесут зависимость к B.

  2. Оценка A inter-thread выполняется перед оценкой B, если A синхронизируется с B, A упорядочена по зависимостям до B или, для некоторой оценки X:

    — A синхронизируется с X, а X располагается перед B,

    - A упорядочен до X, а межпотоковое взаимодействие X происходит до B, или

    — Интерпоток происходит до X, а интерпоток X происходит до B.

  3. ПРИМЕЧАНИЕ 7. Отношение «межпоток происходит раньше» описывает произвольные конкатенации отношений «упорядочить до», «синхронизирует с» и «упорядочить по зависимостям до», с двумя исключениями. Первое исключение состоит в том, что конкатенация не может заканчиваться фразой «предшествующий упорядоченный по зависимости», за которым следует «упорядоченный перед». Причина этого ограничения заключается в том, что операция потребления, участвующая в отношении «упорядочено по зависимости до», обеспечивает упорядочение только в отношении операций, от которых эта операция потребления действительно несет зависимость. Причина, по которой это ограничение применяется только до конца такой конкатенации заключается в том, что любая последующая операция освобождения обеспечит требуемый порядок для предыдущей операции потребления. Второе исключение состоит в том, что конкатенация не может состоять полностью из «последовательности до». Причины этого ограничения заключаются в том, чтобы (1) разрешить транзитивное закрытие «межпотокового события до» и (2) отношение «происходит до», определенное ниже, обеспечивает отношения, полностью состоящие из «упорядоченного до». ''.

  4. Оценка A происходит до оценки B, если A упорядочивается до B или межпотоковая обработка выполняется до B.

  5. Видимый побочный эффект A для объекта M в отношении вычисления значения B объекта M удовлетворяет условиям:

    А происходит раньше Б, и

    - нет другого побочного эффекта от X к M, такого, что A происходит раньше X, а X происходит раньше B.

    Значение неатомарного скалярного объекта M, определенное оценкой B, должно быть значением, сохраненным видимым побочным эффектом A.

(выделение добавлено)


В комментарии ниже я буду сокращать нижеследующее:

  • По порядку зависимостей до: дата рождения
  • Переход между потоками происходит раньше: ITHB
  • Происходит до: HB
  • Последовательность до: SeqB

Давайте рассмотрим, как это применимо. У нас есть 4 соответствующие операции с памятью, которые мы назовем вычислениями A, B, C и D:

Тема 1:

y.store (20);             //    Release; Evaluation A
x.store (10);             //    Release; Evaluation B

Тема 2:

if (x.load() == 10) {     //    Consume; Evaluation C
  assert (y.load() == 20) //    Consume; Evaluation D
  y.store (10)
}

Чтобы доказать, что утверждение никогда не срабатывает, мы пытаемся доказать, что A всегда является видимым побочным эффектом в D. В соответствии с 5.1.2.4 (15) имеем:

A SeqB B DOB C SeqB D

который представляет собой конкатенацию, оканчивающуюся на DOB, за которой следует SeqB. Это явно регулируется (17) не конкатенацией ITHB, несмотря на то, что говорит (16).

Мы знаем, что, поскольку A и D не находятся в одном и том же потоке выполнения, A не является SeqB D; Следовательно, ни одно из двух условий в (18) для HB не выполняется, и A не делает HB D.

Отсюда следует, что A не виден D, так как одно из условий (19) не выполняется. Утверждение может завершиться ошибкой.


Как это может происходить, описано здесь, в обсуждении модели памяти стандарта C++ и здесь, Раздел 4.2 Зависимости управления:

  1. (Некоторое время вперед) Предсказатель ветвления потока 2 предполагает, что будет взят if.
  2. Поток 2 приближается к предсказанной взятой ветке и начинает спекулятивную выборку.
  3. Поток 2 работает не по порядку и предположительно загружает 0xGUNK из y (оценка D). (Может, его еще не выгнали из кеша?).
  4. Поток 1 сохраняет 20 в y (Оценка A)
  5. Поток 1 сохраняет 10 в x (оценка B)
  6. Поток 2 загружает 10 из x (оценка C)
  7. Тема 2 подтверждает, что if взято.
  8. Предполагаемая нагрузка потока 2, равная y == 0xGUNK, фиксируется.
  9. Поток 2 не подтверждается.

Причина, по которой разрешено переупорядочивать оценку D перед C, заключается в том, что consume не запрещает это. Это отличается от acquire-load, который предотвращает изменение порядка загрузки/сохранения после в программном порядке перед. Опять же, в 5.1.2.4(15) указано, что операция потребления, участвующая в отношении «предшествующий порядок зависимостей», обеспечивает упорядочение только в отношении операций, от которых эта операция потребления действительно несет зависимость, и совершенно определенно нет зависимости между двумя нагрузками.


Проверка CppMem

CppMem – это инструмент, помогающий исследовать сценарии доступа к общим данным. в моделях памяти C11 и C++11.

Для следующего кода, который приближается к сценарию в вопросе:

int main() {
  atomic_int x, y;
  y.store(30, mo_seq_cst);
  {{{  { y.store(20, mo_release);
         x.store(10, mo_release); }
  ||| { r3 = x.load(mo_consume).readsvalue(10);
        r4 = y.load(mo_consume); }
  }}};
  return 0; }

Инструмент сообщает о двух непротиворечивых сценариях без гонки, а именно:

Потребление, сценарий успеха

В котором y=20 успешно читается, и

Потребление, сценарий отказа

В котором читается "устаревшее" значение инициализации y=30. Круг от руки мой.

Напротив, когда для нагрузок используется mo_acquire, CppMem сообщает только об одном согласованном сценарии без состязаний, а именно о правильном:

Приобретение, сценарий успеха

в котором читается y=20.

person Iwillnotexist Idonotexist    schedule 17.08.2015
comment
Это действительно полезно! Спасибо! У меня все еще есть глупый вопрос, в примере, который вы привели здесь, почему мы не можем рассуждать о том, что операции A и D образуют отношение выпуска/потребления, которое, согласно 15 и 16, можно считать, что A между потоками происходит до D ? Другими словами, почему мы должны принимать во внимание операции B и C, а не рассуждать только об A и D? - person PJ.Hades; 06.09.2017
comment
@PJ.Hades Пример, который я привел со спекулятивными нагрузками, является примером того, почему. Поскольку процессору разрешено переупорядочивать потребляемые C и D относительно друг друга (при условии, что ни один из них не имеет зависимости друг от друга), D предположительно может произойти задолго до того, как A и B даже начали. Следовательно, чтение C 10 не является доказательством того, что D будет читать 20 (IOW, что синхронизация всегда происходит правильно). Сценарий, который вы описываете (A, D, B, C), приведет к правильной синхронизации, но тот, который я упоминаю (D, A, B, C), также возможен при тех же условиях. - person Iwillnotexist Idonotexist; 06.09.2017
comment
Это имеет смысл. Кажется, я что-то не так понял в определении. Спасибо за объяснение! - person PJ.Hades; 06.09.2017

Оба устанавливают транзитивный порядок «видимости» для атомарных хранилищ, если они не были выпущены с memory_order_relaxed. Если поток читает атомарный объект x в одном из режимов, он может быть уверен, что увидит все модификации всех атомарных объектов y, которые, как известно, были выполнены до записи в x.

Разница между «приобретать» и «потреблять» заключается в видимости неатомарных операций записи в какую-либо переменную z, скажем. Для acquire видны все записи, атомарные или нет. Для consume гарантированно будут видны только атомарные.

thread 1                               thread 2
z = 5 ... store(&x, 3, release) ...... load(&x, acquire) ... z == 5 // we know that z is written
z = 5 ... store(&x, 3, release) ...... load(&x, consume) ... z == ? // we may not have last value of z
person Jens Gustedt    schedule 13.08.2015
comment
Прежде всего спасибо за ваш ответ! Не могли бы вы дать мне ссылку на соответствующую часть стандарта C о том, что все атомарные записи видны для потребления? - person splotz90; 13.08.2015
comment
Если Энтони Уильямс не ошибается в C++ Concurrency in Action p139-140, не все атомарные записи видны после consume. Однако пример в книге Уильямса сочетает в себе упрощенную атомарную запись/чтение с операцией освобождения/потребления. Я не совсем понимаю, почему это должно отличаться для выпуска/потребления + выпуска/потребления, поскольку в примере OP нет порядка зависимостей. - person dyp; 13.08.2015
comment
Хм, хотя это и согласовывает пример в книге с вашим ответом, я не совсем понимаю, как это обеспечивается в спецификации. Насколько я понимаю, нам нужна связь «синхронизация с» между хранилищами и загрузками в x, чтобы вызвать синхронизацию A с X, а X упорядочивается перед правилом B. Но этого нет для выпуска/потребления, и я не вижу другой гарантии, которую мы имеем между двумя хранилищами в потоке 1, кроме внутреннего последовательности до потока, который также применим к не -атомарные операции. - person dyp; 13.08.2015
comment
@ splotz90, все это скрыто в трудно усваиваемом разделе 5.1.2.4. По сути, операции потребления вносят свой вклад в отношение упорядоченного по зависимостям до. Принимая во внимание, что операции получения способствуют межпоточному взаимодействию, которое происходит до отношения, это надмножество этого. (И тогда memory_order_seq_cst это полный порядок всего этого.) - person Jens Gustedt; 13.08.2015
comment
@dyp, memory_order_relaxed операции не входят в число операций синхронизации. В ПРИМЕЧАНИЕ 2 (то есть на стр. 6) стандарта есть объяснение. Они гарантируют только согласованность данных, а не синхронизацию. - person Jens Gustedt; 13.08.2015
comment
Я имел в виду этот конкретный пример; ваш ответ, кажется, предполагает, что существует исключение из синхронизации для неатомарной записи и расслабленной атомарной записи, но я не могу найти такое исключение в спецификации (упорядочение зависимостей даже подразумевает, что некоторые неатомарные записи становятся видимыми, даже если они не встречаются в одном и том же объекте, например, зависимость, которая переносится в p->x из p в p->x). Если такого исключения нет, то должно быть два разных пути для rel+acq и rel+con, но я не могу найти способ для rel+con установить, что происходит-до между хранилищем и загрузкой в ​​y. - person dyp; 13.08.2015
comment
На самом деле у меня та же проблема, чтобы понять, почему стандарт применяет отношение «происходит до» между хранилищем для y и загрузкой из y. - person splotz90; 15.08.2015
comment
@dyp Весь смысл синхронизации в том, чтобы иметь прошлое. Без синхронизма нет ни прошлого, ни будущего: вещи в других потоках могут быть видны, а может и нет. Но даже в будущем никогда не видно, а то, что в прошлом, всегда делается. Если бы неатомарные события не были частью этого прошлого, то создание объекта никогда не вошло бы в историю, и вы даже не могли бы опубликовать объект и предоставить его другому потоку: даже самая простая идиома атомарной публикации (в псевдокоде MT) {x = new T; a.store(true);} ||| { if (a.load()) x->f(); } потерпит неудачу. . - person curiousguy; 23.11.2019
comment
@curiousguy Вау, воскрешаю ветку четырехлетней давности, спасибо за ностальгию :D -- Действительно, я считаю, что ваш пример применим только к выпуску+подтверждению, но не к выпуску+против или расслаблению+что-либо на a - так как есть нет порядка зависимостей между x = new T; и a.store(true). Моделирование этого на CppMem через x = 42; вместо x = new T; показывает, что release+acq не содержит гонок, но два других содержат гонку данных. Вам нужен acq+rel или mutex и т. д., чтобы опубликовать новый объект. Но даже будущее никогда не видно, а то, что было в прошлом, всегда делается. Я не понимаю этого предложения :( - person dyp; 26.11.2019
comment
@dyp Если событие (воспоминание SE) находится в нашем прошлом, мы всегда будем его видеть: эффект либо поддается проверке, либо стирается другим событием, которое не старше. Событие в будущем никогда не видно. Даже ни в том, ни в другом потенциально не может быть замечено. Когда вы программируете только с мьютексами (и без try_lock), вы можете использовать только прошлое, так что все чисто. Когда вы используете атомарные вычисления, вы можете увидеть что-то до того, как это окажется в прошлом: вы можете проверить, был ли атомный объект изменен параллельным вычислением, то есть гонкой. Затем с помощью acq (или другого треда rel) вы можете поместить вещи в прошлое. - person curiousguy; 26.11.2019
comment
@dyp Должна быть возможность использовать потребление для нового объекта, так как это типичный вариант использования. Но авторы компиляторов обнаружили, что очень сложно получить правильную семантику потребления за пределами внешнего интерфейса компилятора в случаях, которые могут быть оптимизированы. (Линус составил список проблемных случаев, когда компиляторы могут нарушать зависимости данных/адреса кода в коде, который считывает такие изменчивые значения: подумайте, x[dep] где x имеет тип int[1], а dep имеет зависимость типа потребления.) - person curiousguy; 26.11.2019
comment
@curiousguy У меня есть некоторые проблемы с пониманием того, что вы хотели бы сделать, и, возможно, может помочь еще немного места (ограничение слов). Поэтому я создал этот чат . - person dyp; 01.12.2019