Разница между 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 действительно может завершиться неудачей.

2 ответов


постановление стандарта C11 выглядит следующим образом.

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

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

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

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

  2. оценка A Интер-поток происходит до оценка B если a синхронизируется с B, A является зависимостью, упорядоченной перед B, или, для некоторой оценки X:

    - a синхронизируется с X и X секвенируется перед B,

    - a секвенируется до того, как произойдет X и X inter-thread перед B или

    - Интер-поток происходит до X и X Интер-поток происходит до B.

  3. Примечание 7 отношение’ inter-thread происходит до "описывает произвольные конкатенации отношений’ sequenced before‘,’ synchronizes with ‘и’ dependency-ordered before",С двумя исключениями. Первым исключением является то, что конкатенация не разрешается заканчиваться ‘зависимостью-упорядоченной до’, за которой следует "sequenced before". Причина этого ограничения заключается в том, что операция потребления, участвующая в связи ‘зависимость упорядочена до’, обеспечивает упорядочение только в отношении операций, к которым эта операция потребления фактически несет зависимость. причина, по которой это ограничение применяется только к концу такой конкатенации, заключается в том, что любая последующая операция выпуска обеспечит необходимый заказ для предыдущей операции потребления. Второе исключение состоит в том, что конкатенация не разрешается полностью состоять из "sequenced before". Причины этого ограничения: (1) разрешить ‘Интер-поток происходит до’ быть транзитивно закрытым и (2) отношение ‘происходит до’, определенное ниже, предусматривает отношения, полностью состоящие из ‘последовательности до’.

  4. оценка A происходит перед оценка B, если a секвенируется до B или Интер-поток случается раньше Б.

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

    - A происходит до B, и

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

    значением неатомного скалярного объекта M, определяемым оценкой B, является значение, сохраненное видимой стороной А. Влияние

(курсив)


в комментарии ниже, я сокращу ниже:

  • зависимость-заказано до: дата рождения
  • Интер-поток происходит раньше: 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 дата рождения 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 подтверждает - это.
  8. спекулятивная нагрузка потока 2 y == 0xGUNK помогут.
  9. поток 2 не утверждать.

причина, по которой разрешено переупорядочивать оценку D перед C, заключается в том, что a потреблять тут не запрещаю. Это не похоже на приобрести-нагрузка, который предотвращает любую нагрузку / магазин после это в порядке программы от переупорядочивания до его. Опять же, в пункте 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; }

инструментом два последовательны, гонки-бесплатные сценарии, а именно:

Consume, Success Scenario

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

Consume, Failure Scenario

в котором "несвежая" инициализация значение y=30 читать. Freehand круг-моя.

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

Acquire, Success Scenario

, в котором y=20 читать.


оба устанавливают транзитивный порядок "видимости" на атомных хранилищах, если они не были выпущены с 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